Compare commits

...

292 Commits

Author SHA1 Message Date
9833d37ebe Merge branch 'dev' of https://github.com/oqtane/oqtane.framework into dev
Some checks failed
build-oqtane / Build the oqtane (push) Has been cancelled
2026-04-11 18:05:16 +02:00
Shaun Walker
6299412fa5 Merge pull request #6168 from sbwalker/dev
add validation to cookie consent form
2026-04-08 16:54:58 -04:00
sbwalker
7c4c6f9c02 add validation to cookie consent form 2026-04-08 16:54:40 -04:00
Shaun Walker
2dfa7b6b2f Merge pull request #6167 from sbwalker/dev
fix #6166 - move UseAntiforgery after UseNotFoundResponse and adjust 404 handling logic
2026-04-08 16:40:29 -04:00
sbwalker
0ea27e7147 fix #6166 - move UseAntiforgery after UseNotFoundResponse and adjust 404 handling logic 2026-04-08 16:40:06 -04:00
Shaun Walker
6852b28b71 Merge pull request #6164 from sbwalker/dev
prevent null reference exception
2026-04-06 15:41:11 -04:00
sbwalker
1741029057 prevent null reference exception 2026-04-06 15:40:46 -04:00
Shaun Walker
a47b8942eb Merge pull request #6163 from sbwalker/dev
fix #6157 - support changing language with LanguageSwitcher when not using content localization
2026-04-06 13:40:29 -04:00
sbwalker
a110497967 fix #6157 - support changing language with LanguageSwitcher when not using content localization 2026-04-06 13:40:06 -04:00
Shaun Walker
59b2a34444 Merge pull request #6156 from sbwalker/dev
allow page order to be specified in @page attribute
2026-03-30 08:56:16 -04:00
sbwalker
6814a7bcdb allow page order to be specified in @page attribute 2026-03-30 08:55:55 -04:00
Shaun Walker
4f7f24a725 Merge pull request #6155 from sbwalker/dev
fix #6154 - handle invalid urls
2026-03-30 08:48:58 -04:00
sbwalker
99949bdeb9 fix #6154 - handle invalid urls 2026-03-30 08:48:33 -04:00
Shaun Walker
26eefa336f Merge pull request #6152 from sbwalker/dev
fix #6148 - ensure cache is refreshed when CultureCode is modified
2026-03-30 08:37:55 -04:00
sbwalker
4c39aadff4 fix #6148 - ensure cache is refreshed when CultureCode is modified 2026-03-30 08:29:47 -04:00
Shaun Walker
3364f18341 Merge pull request #6146 from tvatavuk/patch-5
fix #6145 SynchronizationJob is adding Folder records with null Path...
2026-03-28 07:46:54 -07:00
Tonći Vatavuk
7fd4c617db fix #6145 SynchronizationJob is adding Folder records with null Path during site synchronization 2026-03-27 09:53:53 +01:00
Shaun Walker
a801048ad5 Merge pull request #6143 from oqtane/master
10.1.2 Release
2026-03-26 21:38:41 -07:00
Shaun Walker
d114ae488c Merge pull request #6142 from oqtane/dev
10.1.2 Release
2026-03-26 21:38:23 -07:00
Shaun Walker
2d6b05650b Update latest release to version 10.1.2
Updated the latest release information to version 10.1.2.
2026-03-26 21:37:27 -07:00
Shaun Walker
8ed48c6702 Merge pull request #6141 from sbwalker/dev
update azure deploy package version
2026-03-26 21:33:11 -07:00
sbwalker
6f7789ab3b update azure deploy package version 2026-03-26 21:32:56 -07:00
Shaun Walker
72a684a35a Merge pull request #6140 from sbwalker/dev
add comment related to ISettingsControl usage in Module Settings
2026-03-26 20:19:46 -07:00
sbwalker
f8ca688b2d add comment related to ISettingsControl usage in Module Settings 2026-03-26 20:19:30 -07:00
Shaun Walker
edb3f3750c Merge pull request #6139 from sbwalker/dev
improve help text
2026-03-26 19:42:14 -07:00
sbwalker
96291c4a0e improve help text 2026-03-26 19:38:28 -07:00
Shaun Walker
e9b756092e Merge pull request #6138 from sbwalker/dev
update to .NET SDK 10.0.5 (and latest dependencies)
2026-03-26 19:25:03 -07:00
sbwalker
8349f73e1e update to .NET SDK 10.0.5 (and latest dependencies) 2026-03-26 19:24:38 -07:00
Shaun Walker
7d810ead6c Merge pull request #6137 from sbwalker/dev
increment version to 10.1.2
2026-03-26 19:06:14 -07:00
sbwalker
d01622409a increment version to 10.1.2 2026-03-26 19:05:57 -07:00
Shaun Walker
c01fa810f2 Merge pull request #6136 from oqtane/revert-5968-dev
Revert "fix #5894 - allow user to select upgrade version"
2026-03-26 18:43:08 -07:00
Shaun Walker
64e51038a6 Revert "fix #5894 - allow user to select upgrade version" 2026-03-26 18:42:23 -07:00
Shaun Walker
4e07b41772 Merge pull request #6135 from sbwalker/dev
fix #5951 - add ISettingsControl declaration to Settings component
2026-03-26 18:39:19 -07:00
sbwalker
50b2b80778 fix #5951 - add ISettingsControl declaration to Settings component 2026-03-26 18:38:59 -07:00
Shaun Walker
f6d9300cd0 Merge pull request #6129 from sbwalker/dev
fix #6126 - add alias
2026-03-20 14:58:08 -04:00
sbwalker
ebcc3866ab fix #6126 - add alias 2026-03-20 14:57:43 -04:00
Shaun Walker
36ab6f40c7 Merge pull request #6117 from oqtane/master
10.1.1 Release
2026-03-06 15:49:56 -05:00
Shaun Walker
12809df732 Merge pull request #6116 from oqtane/dev
10.1.1 Release
2026-03-06 15:49:36 -05:00
Shaun Walker
82761530ac Update README.md 2026-03-06 15:48:45 -05:00
Shaun Walker
3eb1542b96 Merge pull request #6115 from sbwalker/dev
update azuredeploy.json
2026-03-06 15:45:11 -05:00
sbwalker
b27b38eccf update azuredeploy.json 2026-03-06 15:44:57 -05:00
Shaun Walker
e3b0bdd500 Merge pull request #6114 from leigh-pointer/Pkgs1011
Bump Radzen, Swashbuckle, MailKit, NodaTime
2026-03-06 15:10:07 -05:00
Leigh Pointer
957309b5f0 Bump Radzen, Swashbuckle, MailKit, NodaTime
Upgrade several dependencies across projects:
- Radzen.Blazor 9.0.0 -> 9.0.8 (Oqtane.Client.csproj and Oqtane.Client.nuspec)
- Swashbuckle.AspNetCore 10.1.2 -> 10.1.4 (Oqtane.Server.csproj and Oqtane.Server.nuspec)
- MailKit 4.14.1 -> 4.15.1 (Oqtane.Server.csproj and Oqtane.Server.nuspec)
- NodaTime 3.3.0 -> 3.3.1 (Oqtane.Shared.csproj and Oqtane.Shared.nuspec)
These are patch/minor dependency updates to incorporate fixes and improvements.
2026-03-06 20:31:17 +01:00
Shaun Walker
c7d32dc5f0 Merge pull request #6113 from sbwalker/dev
improve API
2026-03-06 11:44:45 -05:00
sbwalker
aaffb7b84d improve API 2026-03-06 11:44:29 -05:00
Shaun Walker
0648b23c75 Merge pull request #6112 from sbwalker/dev
fix #6111 - regression issue caused by #6106
2026-03-06 11:19:10 -05:00
sbwalker
f09b21295a fix #6111 - regression issue caused by #6106
Related Work Items: #6
2026-03-06 11:18:50 -05:00
Shaun Walker
f57d296fcb Merge pull request #6107 from sbwalker/dev
rename property to IPortableContext
2026-03-05 13:44:01 -05:00
sbwalker
d2e5ab61da rename property to IPortableContext 2026-03-05 13:43:45 -05:00
Shaun Walker
d7d3273018 Merge pull request #6106 from sbwalker/dev
add property to Module class to indicate how the IPortable interface is being invoked (Export Module, Import Module, Copy Page, Global Replace, Site Template)
2026-03-05 12:55:55 -05:00
sbwalker
c2bb6be2da add property to Module class to indicate how the IPortable interface is being invoked (Export Module, Import Module, Copy Page, Global Replace, Site Template) 2026-03-05 12:55:24 -05:00
Shaun Walker
976d8a3369 Merge pull request #6104 from sbwalker/dev
fix issue deleting Site Group Members
2026-03-04 13:22:07 -05:00
sbwalker
ae9beedca2 fix issue deleting Site Group Members 2026-03-04 13:21:50 -05:00
Shaun Walker
0d668f1469 Merge pull request #6103 from sbwalker/dev
Global Replace should include settings
2026-03-04 10:31:38 -05:00
sbwalker
8de67fdaec Global Replace should include settings 2026-03-04 10:31:21 -05:00
Shaun Walker
a4c94c0dda Merge pull request #6102 from sbwalker/dev
remove unnecessary using
2026-03-04 10:02:24 -05:00
sbwalker
91b15b5e2a remove unnecessary using 2026-03-04 10:02:06 -05:00
Shaun Walker
489629e642 Merge pull request #6101 from sbwalker/dev
prevent update of linked SiteGroup
2026-03-04 10:00:14 -05:00
sbwalker
ef92a88908 prevent update of linked SiteGroup 2026-03-04 09:59:52 -05:00
Shaun Walker
f249801541 Merge pull request #6100 from sbwalker/dev
resolve issue with Change Detection group
2026-03-04 09:09:24 -05:00
sbwalker
147ee8b1e7 resolve issue with Change Detection group 2026-03-04 09:09:07 -05:00
Shaun Walker
263091c27e Merge pull request #6099 from sbwalker/dev
copy page should copy page settings
2026-03-03 17:32:36 -05:00
sbwalker
aac1bb582b copy page should copy page settings 2026-03-03 17:32:19 -05:00
Shaun Walker
b097871d97 Merge pull request #6098 from sbwalker/dev
allow page attributes to support alias names
2026-03-03 15:48:44 -05:00
sbwalker
72542f0146 allow page attributes to support alias names 2026-03-03 15:48:25 -05:00
Shaun Walker
6a4f3bdb8e Merge pull request #6095 from sbwalker/dev
increment version to 10.1.1
2026-03-03 07:55:03 -05:00
sbwalker
e65f61bd65 increment version to 10.1.1 2026-03-03 07:54:45 -05:00
Shaun Walker
2c4dfe0ee0 Merge pull request #6093 from sbwalker/dev
fix #6091 - Add page route incorrect
2026-03-02 13:47:22 -05:00
sbwalker
6160941aee fix #6091 - Add page route incorrect 2026-03-02 13:47:01 -05:00
Shaun Walker
3db3d9561b Merge pull request #6090 from sbwalker/dev
do not reduce length of user agent
2026-02-27 14:36:58 -05:00
sbwalker
cf61a58b4c do not reduce length of user agent 2026-02-27 14:36:43 -05:00
Shaun Walker
e988a9f9c9 Merge pull request #6089 from sbwalker/dev
fix #6087 - module category filter
2026-02-27 14:24:42 -05:00
sbwalker
d1b40f0603 fix #6087 - module category filter 2026-02-27 14:24:26 -05:00
Shaun Walker
772f6ad490 Merge pull request #6088 from sbwalker/dev
use visitor tracking filter with url mapping
2026-02-27 14:17:38 -05:00
sbwalker
f1934028a1 use visitor tracking filter with url mapping 2026-02-27 14:15:03 -05:00
Shaun Walker
5cb0598025 Merge pull request #6085 from sbwalker/dev
add some defensive logic in file handler
2026-02-26 11:04:17 -05:00
sbwalker
18730c5e53 add some defensive logic in file handler 2026-02-26 11:03:58 -05:00
Shaun Walker
f79c3ae994 Merge pull request #6082 from oqtane/master
10.1.0 Release
2026-02-25 14:23:58 -05:00
Shaun Walker
779a3dd379 Merge pull request #6081 from oqtane/dev
10.1.0 Release
2026-02-25 14:23:35 -05:00
Shaun Walker
c3a0a96623 Update README.md 2026-02-25 14:21:58 -05:00
Shaun Walker
f9d6ad2c0f Merge pull request #6080 from sbwalker/dev
update azuredeploy to 10.1.0
2026-02-25 14:17:28 -05:00
sbwalker
1cb0a45715 update azuredeploy to 10.1.0 2026-02-25 14:17:14 -05:00
Shaun Walker
6464604f5c Merge pull request #6077 from sbwalker/dev
provide an indicator in Module Settings when a module is shared across multiple pages
2026-02-25 11:44:37 -05:00
sbwalker
573a914699 provide an indicator in Module Settings when a module is shared across multiple pages 2026-02-25 11:44:15 -05:00
Shaun Walker
ebbe618e98 Merge pull request #6076 from sbwalker/dev
set default module title in control panel when adding or copying existing module
2026-02-25 10:39:23 -05:00
sbwalker
0cc1b5a3e9 set default module title in control panel when adding or copying existing module 2026-02-25 10:38:59 -05:00
Shaun Walker
50ccd29872 Merge pull request #6075 from sbwalker/dev
do not display audit info when copying pages
2026-02-25 09:46:22 -05:00
sbwalker
00d14552b1 do not display audit info when copying pages 2026-02-25 09:46:05 -05:00
Shaun Walker
cad5694145 Merge pull request #6073 from sbwalker/dev
relocate folder hierarchy logic to folder repository (consistent with page approach)
2026-02-25 08:12:55 -05:00
sbwalker
06e555ddd1 relocate folder hierarchy logic to folder repository (consistent with page approach) 2026-02-25 08:12:31 -05:00
Shaun Walker
46aa5225c6 Merge pull request #6070 from sbwalker/dev
explicitly set module order in Default Site Template
2026-02-24 16:05:11 -05:00
sbwalker
09f6a1d531 explicitly set module order in Default Site Template 2026-02-24 16:04:55 -05:00
Shaun Walker
f8633fd390 Merge pull request #6069 from sbwalker/dev
add copy page functionality to control panel
2026-02-24 15:36:01 -05:00
sbwalker
9aad400038 add copy page functionality to control panel 2026-02-24 15:35:45 -05:00
Shaun Walker
91ce840592 Merge pull request #6068 from sbwalker/dev
improve change detection notification logic
2026-02-24 08:59:58 -05:00
sbwalker
458c8534c7 improve change detection notification logic 2026-02-24 08:59:40 -05:00
Shaun Walker
f7df3a4720 Merge pull request #6066 from sbwalker/dev
refactor synchronization job
2026-02-23 08:11:44 -05:00
sbwalker
36789495df refactor synchronization job 2026-02-23 08:11:29 -05:00
Shaun Walker
ed0b341f76 Merge pull request #6063 from sbwalker/dev
allow LanguageSwitcher to support culture and ui culture
2026-02-20 15:12:10 -05:00
sbwalker
0d4d51448e allow LanguageSwitcher to support culture and ui culture 2026-02-20 15:11:54 -05:00
Shaun Walker
3df2c04795 Merge pull request #6062 from sbwalker/dev
fix culture cookie so that it supports culture and ui culture
2026-02-20 14:53:57 -05:00
sbwalker
12f06a7662 fix culture cookie so that it supports culture and ui culture 2026-02-20 14:53:41 -05:00
Shaun Walker
f0cab1ca79 Merge pull request #6060 from sbwalker/dev
handle caching in Global Replace
2026-02-20 08:43:03 -05:00
sbwalker
ae0c4c1099 handle caching in Global Replace 2026-02-20 08:42:41 -05:00
Shaun Walker
dc4ded82b1 Merge pull request #6058 from sbwalker/dev
fix renaming issue
2026-02-19 14:26:01 -05:00
sbwalker
8752b24723 fix renaming issue 2026-02-19 14:25:42 -05:00
Shaun Walker
289252d39c Merge pull request #6056 from sbwalker/dev
support html encoded content
2026-02-19 13:42:47 -05:00
sbwalker
2736fa451c support html encoded content 2026-02-19 13:42:32 -05:00
Shaun Walker
31954a971c Merge pull request #6055 from sbwalker/dev
resolve DbContext issue
2026-02-19 13:27:34 -05:00
sbwalker
a6006ce1fe resolve DbContext issue 2026-02-19 13:27:19 -05:00
Shaun Walker
3db09a2fa6 Merge pull request #6054 from sbwalker/dev
remove unecessary using statements
2026-02-19 12:42:20 -05:00
sbwalker
5c2bd8093a remove unecessary using statements 2026-02-19 12:42:04 -05:00
Shaun Walker
8b8048724a Merge pull request #6053 from sbwalker/dev
change JobTask to SiteTask
2026-02-19 10:47:49 -05:00
sbwalker
060eaa7aff change JobTask to SiteTask 2026-02-19 10:47:30 -05:00
Shaun Walker
67dbc4b7ca Merge pull request #6052 from sbwalker/dev
add Job Tasks to enable the execution of adhoc asynchronous site-based workloads
2026-02-19 08:24:06 -05:00
sbwalker
0fd97d34d9 add Job Tasks to enable the execution of adhoc asynchronous site-based workloads 2026-02-19 08:23:11 -05:00
Shaun Walker
ba51342fd6 Merge pull request #6051 from sbwalker/dev
add new global replace service for bulk updating content
2026-02-18 13:59:41 -05:00
sbwalker
13a58ed099 add new global replace service for bulk updating content 2026-02-18 13:59:25 -05:00
Shaun Walker
48a70c8be3 Merge pull request #6050 from sbwalker/dev
remove unnecessary using
2026-02-17 16:35:30 -05:00
sbwalker
4db58c2866 remove unnecessary using 2026-02-17 16:35:15 -05:00
Shaun Walker
539bad1463 Merge pull request #6049 from sbwalker/dev
performance and scalability improvement - get the most recent HtmlText record directly from database and cache only a single object rather than the entire collection
2026-02-17 16:25:35 -05:00
sbwalker
df7f3f7bba performance and scalability improvement - get the most recent HtmlText record directly from database and cache only a single object rather than the entire collection 2026-02-17 16:25:10 -05:00
Shaun Walker
064448deaf Merge pull request #6047 from Raceeend/issue_6046_filerepository
#6046: FileRepository.GetFile: compare both filenames in the same cast.
2026-02-17 12:19:39 -05:00
Shaun Walker
b25279cdcf Delete Oqtane.Application/Client/Properties/launchSettings.json 2026-02-17 12:19:11 -05:00
Shaun Walker
26c8d00cca Merge pull request #6048 from sbwalker/dev
performance optimization to limit the number of HtmlText content versions
2026-02-17 12:17:20 -05:00
sbwalker
912ed66547 performance optimization to limit the number of HtmlText content versions 2026-02-17 12:16:58 -05:00
Kuyck, Pieter
6f6870b16d #6046: FileRepository.GetFile: compare both filenames in the same cast. 2026-02-17 15:59:20 +01:00
Shaun Walker
1ddf6fe74e Merge pull request #6045 from sbwalker/dev
changed terminology from Comparison to Change Detection for Site Group Type
2026-02-17 09:49:47 -05:00
sbwalker
3e0b5bfa09 changed terminology from Comparison to Change Detection for Site Group Type 2026-02-17 09:49:24 -05:00
Shaun Walker
f60df5c8dc Merge pull request #6044 from sbwalker/dev
improve cache busting for module/theme static assets (ie. do not require a restart)
2026-02-17 09:31:54 -05:00
sbwalker
3af03d308e improve cache busting for module/theme static assets (ie. do not require a restart) 2026-02-17 09:31:13 -05:00
Shaun Walker
7605fd7ec7 Merge pull request #6043 from sbwalker/dev
remove assemblies.log logic
2026-02-16 15:21:02 -05:00
sbwalker
e85b1001c6 remove assemblies.log logic 2026-02-16 15:20:46 -05:00
Shaun Walker
87620f2251 Merge pull request #6042 from sbwalker/dev
add Microsoft.AspNetCore.Authorization to _Imports
2026-02-16 10:30:22 -05:00
sbwalker
772da734dc add Microsoft.AspNetCore.Authorization to _Imports 2026-02-16 10:30:06 -05:00
Shaun Walker
646b6fea84 Merge pull request #6041 from sbwalker/dev
clean up unused resource keys
2026-02-16 08:08:37 -05:00
sbwalker
dd816f7c44 clean up unused resource keys 2026-02-16 08:08:20 -05:00
Shaun Walker
c601c0cdc4 Merge pull request #6040 from sbwalker/dev
add comment for clarification
2026-02-16 08:00:14 -05:00
sbwalker
a4f7d1f745 add comment for clarification 2026-02-16 07:59:55 -05:00
Shaun Walker
a4980eac33 Merge pull request #6039 from sbwalker/dev
resolve permission issue
2026-02-15 13:46:01 -05:00
sbwalker
d3c4e78baa resolve permission issue 2026-02-15 13:45:45 -05:00
Shaun Walker
5bf5d2d515 Merge pull request #6038 from sbwalker/dev
modifications for page attributes
2026-02-13 16:57:11 -05:00
sbwalker
657c6620d5 modifications for page attributes 2026-02-13 16:56:54 -05:00
Shaun Walker
de5725b92c Merge pull request #6036 from sbwalker/dev
provide support for @page route
2026-02-13 10:50:24 -05:00
sbwalker
d9db41ac99 provide support for @page route 2026-02-13 10:50:03 -05:00
Shaun Walker
dcd862900f Merge pull request #6031 from oqtane/revert-6027-revert-6012-IdentifyServices
Rename services to Client/Server and update refs
2026-02-11 14:20:44 -05:00
Shaun Walker
0f8d22e6f0 Merge pull request #6032 from oqtane/revert-6028-revert-6013-AppServcieInterface
Rename client and server module services
2026-02-11 14:16:52 -05:00
Shaun Walker
0d75e0f555 Revert "Revert "Rename client and server module services"" 2026-02-11 14:12:15 -05:00
Shaun Walker
84e8edb159 Revert "Revert "Rename services to Client/Server and update refs"" 2026-02-11 14:11:22 -05:00
Shaun Walker
edb6109db9 Merge pull request #6030 from sbwalker/dev
remove System.Net.Http.Json from default moduile template
2026-02-11 14:08:14 -05:00
sbwalker
4cc9348769 remove System.Net.Http.Json from default moduile template 2026-02-11 14:07:57 -05:00
Shaun Walker
1a6420ee45 Merge pull request #6029 from sbwalker/dev
update default module/theme template solution files to avoid building the linked Oqtane.Server project
2026-02-11 14:04:13 -05:00
sbwalker
486132f918 update default module/theme template solution files to avoid building the linked Oqtane.Server project 2026-02-11 14:03:46 -05:00
Shaun Walker
a18f317e85 Merge pull request #6028 from oqtane/revert-6013-AppServcieInterface
Revert "Rename client and server module services"
2026-02-11 13:54:25 -05:00
Shaun Walker
171735fac4 Revert "Rename client and server module services" 2026-02-11 13:54:10 -05:00
Shaun Walker
72d4361fb2 Merge pull request #6027 from oqtane/revert-6012-IdentifyServices
Revert "Rename services to Client/Server and update refs"
2026-02-11 13:52:51 -05:00
Shaun Walker
69fc1c9895 Revert "Rename services to Client/Server and update refs" 2026-02-11 13:52:35 -05:00
Shaun Walker
831bc31e06 Merge pull request #6013 from leigh-pointer/AppServcieInterface
Rename client and server module services
2026-02-11 13:46:01 -05:00
Shaun Walker
68445c40f7 Merge pull request #6012 from leigh-pointer/IdentifyServices
Rename services to Client/Server and update refs
2026-02-11 13:45:45 -05:00
Shaun Walker
2b2d960c3e Merge branch 'dev' into IdentifyServices 2026-02-11 13:45:36 -05:00
Shaun Walker
d687d81d0c Merge pull request #6026 from sbwalker/dev
improve content localization
2026-02-11 11:21:25 -05:00
sbwalker
2d19f21b2e improve content localization 2026-02-11 11:21:05 -05:00
Shaun Walker
e85a2a6cf5 Merge pull request #6025 from sbwalker/dev
improve user delete experience
2026-02-11 10:20:39 -05:00
sbwalker
6d35d40829 improve user delete experience 2026-02-11 10:20:19 -05:00
Shaun Walker
4b3377cc78 Merge pull request #6024 from sbwalker/dev
increment version to 10.1.0
2026-02-11 10:06:09 -05:00
sbwalker
971ba796ef increment version to 10.1.0 2026-02-11 10:05:46 -05:00
Shaun Walker
3195e4b46f Merge pull request #6023 from sbwalker/dev
update to .NET SDK 10.0.3 (and latest dependencies)
2026-02-11 09:23:48 -05:00
sbwalker
10c1779f84 update to .NET SDK 10.0.3 (and latest dependencies) 2026-02-11 09:23:23 -05:00
Shaun Walker
b5c9717f71 Merge pull request #6022 from sbwalker/dev
improvements to ISynchronizable
2026-02-11 08:59:27 -05:00
sbwalker
d13e6fcdad improvements to ISynchronizable 2026-02-11 08:59:10 -05:00
Shaun Walker
357337572e Merge pull request #6021 from sbwalker/dev
site group improvements
2026-02-10 13:16:27 -05:00
sbwalker
e95a6b774e site group improvements 2026-02-10 13:16:12 -05:00
Shaun Walker
59bdc4c04e Merge pull request #6019 from sbwalker/dev
improvements to site synchronization
2026-02-10 12:32:26 -05:00
sbwalker
ad300a58c1 improvements to site synchronization 2026-02-10 12:32:09 -05:00
Shaun Walker
8f25d9f06a Merge pull request #6018 from sbwalker/dev
improve log messages
2026-02-10 09:13:56 -05:00
sbwalker
3dcb391a14 improve log messages 2026-02-10 09:13:39 -05:00
Shaun Walker
9073b8a9ff Merge pull request #6017 from sbwalker/dev
improvements to site groups
2026-02-10 08:55:28 -05:00
sbwalker
6f2e676c00 improvements to site groups 2026-02-10 08:55:11 -05:00
Shaun Walker
dbd9388d4c Merge pull request #6016 from sbwalker/dev
refactoring of site groups
2026-02-09 14:43:52 -05:00
sbwalker
ddd6dfc475 refactoring of site groups 2026-02-09 13:58:38 -05:00
Shaun Walker
16e2efdd66 Merge pull request #6015 from sbwalker/dev
handle cache invalidation for site groups
2026-02-06 16:07:23 -05:00
sbwalker
c0e191537f handle cache invalidation for site groups 2026-02-06 16:07:07 -05:00
Shaun Walker
1652afc10f Merge pull request #6014 from sbwalker/dev
fix issue when loading languages for content localization
2026-02-06 15:39:48 -05:00
sbwalker
aab0dd96dd fix issue when loading languages for content localization 2026-02-06 15:37:24 -05:00
Leigh Pointer
783d01bf9f Rename client and server module services
Rename the client service class to Client[Module]Service and the server service file to Server[Module]Service, updating all references accordingly. Updated DI registration in ClientStartup to register Client[Module]Service, and updated Edit.razor and Index.razor to inject and call Client[Module]Service instead of [Module]Service. This clarifies client vs server implementations and avoids naming collisions.
2026-02-06 18:31:58 +01:00
Leigh Pointer
60b2e50511 Rename services to Client/Server and update refs
Rename the client service class/file from [Module]Service to Client[Module]Service and the server service to Server[Module]Service. Update Edit.razor and Index.razor to inject Client[Module]Service and adjust all calls accordingly. Update DI registration in ClientStartup to register Client[Module]Service. Also remove the System.Net.Http.Json package reference from the client .csproj. File renames and reference updates keep class names and registrations consistent between client and server templates.
2026-02-06 18:25:21 +01:00
Shaun Walker
6520f8ade4 Merge pull request #6011 from sbwalker/dev
improvements for site groups
2026-02-06 11:53:28 -05:00
sbwalker
57deeb6acf improvements for site groups 2026-02-06 11:53:10 -05:00
Shaun Walker
49102491a6 Merge pull request #6010 from sbwalker/dev
improve support for BodyContent
2026-02-05 15:33:24 -05:00
sbwalker
dff2261994 improve support for BodyContent 2026-02-05 15:33:08 -05:00
Shaun Walker
daca055f90 Merge pull request #6009 from sbwalker/dev
additional validation
2026-02-05 14:30:21 -05:00
sbwalker
b2ef3cc574 additional validation 2026-02-05 14:30:06 -05:00
Shaun Walker
8bd035b8f4 Merge pull request #6007 from sbwalker/dev
add new method for getting neutral cultures
2026-02-05 13:23:30 -05:00
sbwalker
8817af42bd add new method for getting neutral cultures 2026-02-05 13:23:14 -05:00
Shaun Walker
7b1a12f1e7 Merge pull request #6002 from leigh-pointer/patch-2
Enable CopyLocalLockFileAssemblies in project file
2026-02-05 07:58:21 -05:00
Leigh Pointer
d313c2f3a5 Enable CopyLocalLockFileAssemblies in project file 2026-02-03 19:14:51 +01:00
Shaun Walker
831f7c5738 Merge pull request #5999 from leigh-pointer/AppServcieInterface
Move I[Module]Service to Shared/Interfaces
2026-01-30 14:59:34 -05:00
Shaun Walker
7bb84fbe33 Merge pull request #5996 from leigh-pointer/ModTempInterface
Move I[Module]Service to Shared
2026-01-30 14:59:24 -05:00
Leigh Pointer
0dc9382215 Move I[Module]Service to Shared/Interfaces
Extract the I[Module]Service interface out of Client/Services/[Module]Service.cs and add it as a new shared interface file at Internal/Shared/Interfaces/I[Module]Service.cs. The inline interface definition was removed from [Module]Service.cs so the service class now implements the shared contract, centralizing the interface for reuse across components.
2026-01-30 17:59:29 +01:00
Leigh Pointer
563696c7a5 Removed the Reference to the External Client in the External Server project 2026-01-30 17:37:35 +01:00
Shaun Walker
66963a6508 Merge pull request #5998 from sbwalker/dev
change order of operations when saving in edit page
2026-01-30 09:17:28 -05:00
sbwalker
8ec4922cd3 change order of operations when saving in edit page 2026-01-30 09:17:13 -05:00
Shaun Walker
ed7d1eef14 Merge pull request #5997 from sbwalker/dev
fix #5988 - add inline script support to static rendering
2026-01-30 09:01:14 -05:00
sbwalker
7ab1184a04 fix #5988 - add inline script support to static rendering 2026-01-30 09:00:58 -05:00
Leigh Pointer
d638f9492d Move I[Module]Service to Shared
Relocate the I[Module]Service interface from the client Services file to a new Shared/Interfaces/I[Module]Service.cs so the interface becomes a shared contract. Remove the duplicate interface from the client-side [Module]Service implementation.
It was move in error, release 6.1.5 PR add a new Visual Studio Project Template #5493
2026-01-30 14:03:21 +01:00
Shaun Walker
6d4870f41a Merge pull request #5995 from sbwalker/dev
fix #5951 change order of operations when saving module settings
2026-01-29 16:39:06 -05:00
sbwalker
81f4f87493 fix #5951 change order of operations when saving module settings 2026-01-29 16:38:45 -05:00
Shaun Walker
4d1ba921dc Merge pull request #5994 from sbwalker/dev
add defensive logic
2026-01-29 11:13:02 -05:00
sbwalker
d6458eeaf6 add defensive logic 2026-01-29 11:12:45 -05:00
Shaun Walker
f4f935ab43 Merge pull request #5993 from sbwalker/dev
resolve issue replicating files
2026-01-29 10:59:47 -05:00
sbwalker
738ad9bbfa resolve issue replicating files 2026-01-29 10:59:31 -05:00
Shaun Walker
a470db594e Merge pull request #5992 from sbwalker/dev
refactor Site Groups
2026-01-28 19:06:33 -05:00
sbwalker
ce905499b0 refactor Site Groups 2026-01-28 19:06:18 -05:00
Shaun Walker
7e6b60405b Merge pull request #5990 from sbwalker/dev
add last synchronization date
2026-01-28 13:59:50 -05:00
sbwalker
8a4275c240 add last synchronization date 2026-01-28 13:59:35 -05:00
Shaun Walker
5735f65628 Merge pull request #5986 from ijaz-saeed/master
admin panel, new entry
2026-01-28 10:48:44 -05:00
Shaun Walker
8749ab7588 Merge pull request #5989 from sbwalker/dev
refactorimg Site Groups
2026-01-28 10:47:57 -05:00
sbwalker
912c01cdf8 refactorimg Site Groups 2026-01-28 10:47:39 -05:00
isaeed
d6bbc6b82f admin panel, new entry 2026-01-28 12:40:17 +05:00
Shaun Walker
1588d4626b Merge pull request #5985 from sbwalker/dev
only host users can synchronize sites
2026-01-27 17:05:47 -05:00
sbwalker
c95725d444 only host users can synchronize sites 2026-01-27 17:05:31 -05:00
Shaun Walker
d3e4c57ede Merge pull request #5981 from mdmontesinos/max-upload-size
Maximum upload file size parameter for FileManager
2026-01-27 16:53:00 -05:00
Shaun Walker
e34013ace5 Merge pull request #5984 from sbwalker/dev
introducing Site Groups
2026-01-27 16:51:48 -05:00
sbwalker
3be2b9c720 introducing Site Groups 2026-01-27 16:51:30 -05:00
David Montesinos
256af74e0c Maximum upload file size parameter for FileManager 2026-01-23 09:34:06 +01:00
Shaun Walker
0295b66c22 Update README.md 2026-01-20 15:07:42 -05:00
Shaun Walker
3393ece21d Merge pull request #5976 from oqtane/master
10.0.4 Release
2026-01-20 14:56:48 -05:00
Shaun Walker
1ac722bd06 Merge pull request #5975 from oqtane/dev
10.0.4 Release
2026-01-20 14:56:21 -05:00
Shaun Walker
d0fb8dc5fc Update README.md 2026-01-20 14:55:32 -05:00
Shaun Walker
6006e6f63c Merge pull request #5974 from sbwalker/dev
update azuredeploy.json
2026-01-20 14:51:45 -05:00
sbwalker
0a3f60c007 undo previous commit 2026-01-20 14:50:51 -05:00
sbwalker
472e8eadec update azuredeploy.json 2026-01-20 14:44:06 -05:00
Shaun Walker
d2c515d837 Merge pull request #5973 from sbwalker/dev
use primary database type if type not explicitly specified
2026-01-19 12:42:41 -05:00
sbwalker
0446bdb970 use primary database type if type not explicitly specified 2026-01-19 12:42:24 -05:00
Shaun Walker
adf2468373 Merge pull request #5972 from sbwalker/dev
include message to notify user of change in connection name format
2026-01-19 11:57:09 -05:00
sbwalker
54d3f1c659 include message to notify user of change in connection name format 2026-01-19 11:56:54 -05:00
Shaun Walker
e3b45a2329 Merge pull request #5971 from sbwalker/dev
allow SQL Management to support non-tenant databases
2026-01-19 11:36:47 -05:00
sbwalker
1a777b29e0 allow SQL Management to support non-tenant databases 2026-01-19 11:36:27 -05:00
Shaun Walker
0c3754ca86 Merge pull request #5970 from sbwalker/dev
remove SMTP Relay setting
2026-01-19 10:45:18 -05:00
sbwalker
b0211b2e6e remove SMTP Relay setting 2026-01-19 10:45:02 -05:00
Shaun Walker
20548710ce Merge pull request #5969 from sbwalker/dev
fix #5965 - validate user registration before adding user to a site
2026-01-19 10:13:17 -05:00
sbwalker
92f4a8b683 fix #5965 - validate user registration before adding user to a site 2026-01-19 10:13:01 -05:00
Shaun Walker
c1709db676 Merge pull request #5968 from sbwalker/dev
fix #5894 - allow user to select upgrade version
2026-01-19 08:25:14 -05:00
sbwalker
7d7ecf4757 fix #5894 - allow user to select upgrade version 2026-01-19 08:24:56 -05:00
Shaun Walker
f1c85d23d6 Merge pull request #5964 from sbwalker/dev
enable scheduled job automatically if SMTP is enabled in Site Settings
2026-01-16 11:58:21 -05:00
sbwalker
780e2a8484 enable scheduled job automatically if SMTP is enabled in Site Settings 2026-01-16 11:58:03 -05:00
Shaun Walker
0624d539bf Merge pull request #5963 from sbwalker/dev
utilize ReplyTo in NotificationJob
2026-01-16 11:35:49 -05:00
sbwalker
e98d84784a utilize ReplyTo in NotificationJob 2026-01-16 11:35:30 -05:00
Shaun Walker
1678817bf0 Merge pull request #5962 from zyhfish/task/fix-5961
Fix #5961: redirect to the external login page automatically.
2026-01-16 11:20:52 -05:00
Shaun Walker
bf0fb0cbae Merge pull request #5960 from zyhfish/task/fix-5959
Fix #5959: set OnRedirectToIdentityProvider events without overwrite previous settings.
2026-01-16 11:20:41 -05:00
Ben
9dc91aec58 Fix #5961: redirect to the external login page automatically. 2026-01-16 11:02:46 +08:00
Ben
8ba480f168 Fix #5959: set OnRedirectToIdentityProvider events without overwrite previous settings. 2026-01-16 10:28:59 +08:00
Ben
9d42dac7d9 Fix #5959: set OnRedirectToIdentityProvider events without overwrite previous settings. 2026-01-16 10:26:49 +08:00
Shaun Walker
236aa84f87 Merge pull request #5958 from sbwalker/dev
fix #5910 - eliminate double render of Login component
2026-01-14 15:54:57 -05:00
sbwalker
a6f7ec9bd5 fix #5910 - eliminate double render of Login component 2026-01-14 15:54:39 -05:00
Shaun Walker
2bf8c947b6 Merge pull request #5957 from sbwalker/dev
improve SMTP Relay description
2026-01-14 11:11:58 -05:00
sbwalker
c8872a5e99 improve SMTP Relay description 2026-01-14 11:11:42 -05:00
Shaun Walker
36f04708ba Merge pull request #5956 from sbwalker/dev
update license copyright date
2026-01-14 10:11:37 -05:00
sbwalker
3b990aa633 update license copyright date 2026-01-14 10:11:23 -05:00
Shaun Walker
3192ab8452 Merge pull request #5955 from sbwalker/dev
update copyright date
2026-01-14 10:08:59 -05:00
sbwalker
51638fdcb0 update copyright date 2026-01-14 10:08:44 -05:00
Shaun Walker
95f98b89fe Merge pull request #5954 from sbwalker/dev
update to .NET SDK 10.0.2 (and latest dependencies)
2026-01-14 09:51:55 -05:00
sbwalker
6c484ea9cd update to .NET SDK 10.0.2 (and latest dependencies) 2026-01-14 09:51:36 -05:00
Shaun Walker
ea0271ba79 Merge pull request #5949 from zyhfish/task/fix-5948
Fix #5948: re-render the module message when it's been changed.
2026-01-14 08:21:19 -05:00
Shaun Walker
526b797d03 Merge pull request #5953 from sbwalker/dev
improve Edit Job  UI
2026-01-14 08:20:48 -05:00
sbwalker
0d0efaf4ca improve Edit Job UI 2026-01-14 08:20:30 -05:00
f3175b6b06 Update .gitea/workflows/build-oqtane.yml
Some checks failed
build-oqtane / Build the oqtane (push) Has been cancelled
2026-01-13 20:42:35 +00:00
90e254fae6 Update .gitea/workflows/build-container.yml 2026-01-13 20:40:37 +00:00
4a7d088612 Merge tag 'v10.0.3' into dev 2026-01-13 11:08:55 +01:00
Ben
5c70f4f6a7 Fix #5948: re-render the module message when it's been changed. 2026-01-09 08:44:13 +08:00
Shaun Walker
b0d624034a Merge pull request #5947 from zyhfish/task/fix-5942
Fix #5942: set the next  execution time correctly.
2026-01-08 16:09:46 -05:00
Ben
43ee682d1f Fix #5942: set the next execution time correctly. 2026-01-08 11:39:32 +08:00
Shaun Walker
d4399955ac Merge pull request #5944 from sbwalker/dev
fix #5940 - add MySQL support to Oqtane 10
2026-01-05 15:50:01 -05:00
sbwalker
86f88c4f7c fix #5940 - add MySQL support to Oqtane 10 2026-01-05 15:49:42 -05:00
Shaun Walker
d6ea610764 Merge pull request #5939 from leigh-pointer/TabSecurity
Refine tab visibility authorization logic
2026-01-02 09:04:25 -05:00
Leigh Pointer
ff8417ed31 Refine tab visibility authorization logic
Updated the tab visibility logic to clarify and enforce the authorization hierarchy. Host-only tabs now strictly require the Host role, Admins bypass all checks except Host, and null SecurityAccessLevel still enforces RoleName and PermissionName if specified. Improved code comments for clarity and adjusted logic to ensure correct permission checks.
2025-12-31 11:10:10 +01:00
Shaun Walker
bf49e52cbb Merge pull request #5938 from sbwalker/dev
increment version to 10.0.4
2025-12-30 13:43:37 -05:00
sbwalker
2844b793fa increment version to 10.0.4 2025-12-30 13:43:11 -05:00
Shaun Walker
9218863dae Merge pull request #5937 from sbwalker/dev
fix #5930 - NotificationJob not sending emails (reverting #5848)
2025-12-30 08:33:51 -05:00
sbwalker
ef20d870ee fix #5930 - NotificationJob not sending emails (reverting #5848) 2025-12-30 08:33:28 -05:00
Shaun Walker
76c79206b6 Merge pull request #5935 from sbwalker/dev
clarify SMTP Relay site option to avoid confusion
2025-12-29 11:08:37 -05:00
sbwalker
b4c6b6b794 clarify SMTP Relay site option to avoid confusion 2025-12-29 11:08:10 -05:00
29ac9334ba revert 3f90653894
revert Wurde zu einem feedback website ummodeliert
2025-10-14 05:47:57 +00:00
3f90653894 Wurde zu einem feedback website ummodeliert 2025-09-26 11:20:56 +02:00
ea056165ca Merge tag 'v6.2.0' into dev 2025-09-25 13:38:07 +02:00
7a9941fe66 Add entrypoint, modify Dockerfile: Overwrite volume files 2025-05-30 17:40:18 +02:00
b3b39f583a Merge mirror with local history. 2025-05-30 11:51:43 +02:00
92c554e854 New: Register Component that Renders a Link (secondary) to the register URL including the redirect property. 2025-05-30 11:45:26 +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
178 changed files with 5644 additions and 1585 deletions

View File

@@ -0,0 +1,23 @@
name: build-oqtane
on:
- push
jobs:
build:
name: Build the oqtane
runs-on: mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64
steps:
- name: "Git clone"
run: git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
- name: "Git checkout"
run: git checkout "${{ gitea.sha }}"
- name: "Oqtane Framework bauen"
run: dotnet build -c Release ./oqtane.framework/Oqtane.slnx
- name: "Oqtane Framework publish"
run: dotnet publish -c Release ./oqtane.framework/Oqtane.slnx -o ./out
- name: Upload Package
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
name: oqtane.framework-amd64
path: ./out

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>10.0.3</Version>
<Version>10.1.2</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/v10.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
</PropertyGroup>

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# 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 /codefiles
COPY --from=publish /source/publish/ /codefiles/
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2025 .NET Foundation
Copyright (c) 2018-2026 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -12,10 +12,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.3" />
<PackageReference Include="Oqtane.Client" Version="10.1.2" />
</ItemGroup>
</Project>

View File

@@ -9,6 +9,7 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using Oqtane
@using Oqtane.Models

View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>Oqtane.Application.Template</id>
<version>10.0.3</version>
<version>10.1.2</version>
<title>Oqtane Application Template For Blazor</title>
<authors>Shaun Walker</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@@ -22,9 +22,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
@@ -33,7 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.3" />
<PackageReference Include="Oqtane.Server" Version="10.1.2" />
</ItemGroup>
</Project>

View File

@@ -4,7 +4,7 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject I[Module]Service Client[Module]Service
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@@ -50,7 +50,7 @@
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
if ([Module] != null)
{
_name = [Module].Name;
@@ -81,14 +81,14 @@
[Module] [Module] = new [Module]();
[Module].ModuleId = ModuleState.ModuleId;
[Module].Name = _name;
[Module] = await [Module]Service.Add[Module]Async([Module]);
[Module] = await Client[Module]Service.Add[Module]Async([Module]);
await logger.LogInformation("[Module] Added {[Module]}", [Module]);
}
else
{
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module].Name = _name;
await [Module]Service.Update[Module]Async([Module]);
await Client[Module]Service.Update[Module]Async([Module]);
await logger.LogInformation("[Module] Updated {[Module]}", [Module]);
}
NavigationManager.NavigateTo(NavigateUrl());

View File

@@ -3,7 +3,7 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject I[Module]Service Client[Module]Service
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer
@@ -52,7 +52,7 @@ else
{
try
{
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
_[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
}
catch (Exception ex)
{
@@ -65,9 +65,9 @@ else
{
try
{
await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
await logger.LogInformation("[Module] Deleted {[Module]}", [Module]);
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
_[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
StateHasChanged();
}
catch (Exception ex)

View File

@@ -1,5 +1,6 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@inject IStringLocalizer<Settings> Localizer

View File

@@ -7,22 +7,10 @@ using Oqtane.Shared;
namespace [Owner].Module.[Module].Services
{
public interface I[Module]Service
public class Client[Module]Service : ServiceBase, I[Module]Service
{
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
public class [Module]Service : ServiceBase, I[Module]Service
{
public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("[Module]");

View File

@@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup
{
if (!services.Any(s => s.ServiceType == typeof(I[Module]Service)))
{
services.AddScoped<I[Module]Service, [Module]Service>();
services.AddScoped<I[Module]Service, Client[Module]Service>();
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Services
{
public interface I[Module]Service
{
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
}

View File

@@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="10.0.3" />
<PackageReference Include="Oqtane.Shared" Version="10.1.2" />
</ItemGroup>
</Project>

View File

@@ -57,6 +57,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<ISiteGroupService, SiteGroupService>();
services.AddScoped<ISiteGroupMemberService, SiteGroupMemberService>();
services.AddScoped<ISiteTaskService, SiteTaskService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers

View File

@@ -0,0 +1,117 @@
@namespace Oqtane.Modules.Admin.GlobalReplace
@using System.Text.Json
@inherits ModuleBase
@inject ISiteTaskService SiteTaskService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="find" HelpText="Specify the content which needs to be replaced" ResourceKey="Find">Find What: </Label>
<div class="col-sm-9">
<input id="find" class="form-control" @bind="@_find" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="replace" HelpText="Specify the replacement content" ResourceKey="Replace">Replace With: </Label>
<div class="col-sm-9">
<input id="replace" class="form-control" @bind="@_replace" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="casesensitive" HelpText="Specify if the replacement operation should be case sensitive" ResourceKey="CaseSensitive">Match Case? </Label>
<div class="col-sm-9">
<select id="casesensitive" class="form-select" @bind="@_caseSensitive">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="site" HelpText="Specify if site information should be updated (ie. name, head content, body content, settings)" ResourceKey="Site">Site Info? </Label>
<div class="col-sm-9">
<select id="site" class="form-select" @bind="@_site">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pages" HelpText="Specify if page information should be updated (ie. name, title, head content, body content, settings)" ResourceKey="Pages">Page Info? </Label>
<div class="col-sm-9">
<select id="pages" class="form-select" @bind="@_pages">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="modules" HelpText="Specify if module information should be updated (ie. title, header, footer, settings)" ResourceKey="Modules">Module Info? </Label>
<div class="col-sm-9">
<select id="modules" class="form-select" @bind="@_modules">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Specify if module content should be updated" ResourceKey="Content">Module Content? </Label>
<div class="col-sm-9">
<select id="content" class="form-select" @bind="@_content">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br /><br />
<ActionDialog Header="Global Replace" Message="This Operation is Permanent. Are You Sure You Wish To Proceed?" Action="Replace" Class="btn btn-primary" OnClick="@(async () => await Save())" ResourceKey="GlobalReplace" />
<br /><br />
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override string Title => "Global Replace";
private string _find;
private string _replace;
private string _caseSensitive = "True";
private string _site = "True";
private string _pages = "True";
private string _modules = "True";
private string _content = "True";
private async Task Save()
{
try
{
if (!string.IsNullOrEmpty(_find) && !string.IsNullOrEmpty(_replace))
{
var replace = new GlobalReplace
{
Find = _find,
Replace = _replace,
CaseSensitive = bool.Parse(_caseSensitive),
Site = bool.Parse(_site),
Pages = bool.Parse(_pages),
Modules = bool.Parse(_modules),
Content = bool.Parse(_content)
};
var siteTask = new SiteTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace));
await SiteTaskService.AddSiteTaskAsync(siteTask);
AddModuleMessage(Localizer["Success.Save"], MessageType.Success);
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Global Replace Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Save"], MessageType.Error);
}
}
}

View File

@@ -5,100 +5,109 @@
@inject IStringLocalizer<Edit> 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="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required />
@if (_initialized)
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
@if (!_editable)
{
<ModuleMessage Message="@Localizer["JobNotEditable"]" Type="MessageType.Warning" />
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" required disabled />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" readonly />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required disabled="@(!_editable)" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<div class="col-sm-9">
<select id="enabled" class="form-select" @bind="@_isEnabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<div class="col-sm-9">
<select id="enabled" class="form-select" @bind="@_isEnabled" required disabled="@(!_editable)">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<div class="col-sm-9">
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required />
<select id="runs-every" class="form-select" @bind="@_frequency" required>
<option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<div class="col-sm-9">
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required disabled="@(!_editable)" />
<select id="runs-every" class="form-select" @bind="@_frequency" required disabled="@(!_editable)">
<option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" type="number" min="0" step="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required disabled="@(!_editable)" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div>
<div class="col">
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="starting" type="date" class="form-control" @bind="@_startDate" disabled="@(!_editable)" />
</div>
<div class="col">
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" disabled="@(!_editable)" />
</div>
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div>
<div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="ending" type="date" class="form-control" @bind="@_endDate" disabled="@(!_editable)" />
</div>
<div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" disabled="@(!_editable)" />
</div>
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="next" HelpText="The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job." ResourceKey="NextExecution">Next Execution: </Label>
<div class="col-sm-9">
<input id="next" class="form-control" @bind="@_nextDate" disabled />
</div>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
<br />
@if (_editable)
{
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
}
else
{
<button type="button" class="btn btn-danger" @onclick="DisableJob">@Localizer["Disable"]</button>
}
<NavLink class="btn btn-secondary ms-1" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
}
@code {
private ElementReference form;
private bool validated = false;
private bool _initialized = false;
private bool _editable = true;
private int _jobId;
private string _name = string.Empty;
private string _jobType = string.Empty;
@@ -113,26 +122,32 @@
private DateTime? _nextDate = null;
private DateTime? _nextTime = null;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
protected override async Task OnInitializedAsync()
{
await LoadJob();
}
protected async Task LoadJob()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_editable = !job.IsEnabled && !job.IsExecuting;
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate);
@@ -140,70 +155,107 @@
_nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn;
}
private async Task SaveJob()
{
if (!Utilities.ValidateEffectiveExpiryDates(_startDate, _endDate))
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
return;
_initialized = true;
}
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
private async Task SaveJob()
{
try
{
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.IsEnabled = bool.Parse(_isEnabled);
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
try
{
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory);
if (!job.IsEnabled || Utilities.ValidateEffectiveExpiryDates(job.StartDate, job.EndDate))
{
if (!job.IsEnabled || (job.StartDate >= DateTime.UtcNow || job.StartDate == null))
{
job.NextExecution = null;
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.StartDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
private async Task DisableJob()
{
try
{
var job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
if (job.IsExecuting)
{
AddModuleMessage(Localizer["Message.ExecutingError"], MessageType.Warning);
}
else
{
job.IsEnabled = false;
job.NextExecution = null;
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
}

View File

@@ -144,7 +144,6 @@ else
private string _code = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override bool? Prerender => true;
public override List<Resource> Resources => new List<Resource>()
{
@@ -222,6 +221,14 @@ else
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning);
}
else
{
if (_allowexternallogin && !_allowsitelogin)
{
// external login
ExternalLogin();
}
}
}
}
catch (Exception ex)

View File

@@ -46,7 +46,8 @@
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information">
<br />
<Section Name="Information" ResourceKey="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduledefinitionname" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
@@ -97,7 +98,13 @@
}
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="fingerprint" HelpText="A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting)." ResourceKey="Fingerprint">Fingerprint: </Label>
<div class="col-sm-9">
<input id="fingerprint" class="form-control" @bind="@_fingerprint" disabled />
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
@@ -231,6 +238,7 @@
private string _url = "";
private string _contact = "";
private string _license = "";
private string _fingerprint = "";
private List<Permission> _permissions = null;
private string _createdby;
private DateTime _createdon;
@@ -266,6 +274,7 @@
_url = moduleDefinition.Url;
_contact = moduleDefinition.Contact;
_license = moduleDefinition.License;
_fingerprint = moduleDefinition.Fingerprint;
_permissions = moduleDefinition.PermissionList;
_createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn;

View File

@@ -130,7 +130,7 @@ else
private async Task LoadModuleDefinitions()
{
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_packages = await PackageService.GetPackageUpdatesAsync("module");
}

View File

@@ -73,27 +73,29 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label>
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on. Please note that shared modules cannot be moved to other pages." ResourceKey="Page">Page: </Label>
<div class="col-sm-9">
<select id="page" class="form-select" @bind="@_pageId" required>
@if (PageState.Page.UserId != null)
{
@if (PageState.Page.UserId != null || _isShared)
{
<select id="page" class="form-select" @bind="@_pageId" required disabled>
<option value="@PageState.Page.PageId">@(PageState.Page.Name)</option>
}
else
</select>
}
else
{
<select id="page" class="form-select" @bind="@_pageId" required>
@if (_pages != null)
{
if (_pages != null)
foreach (Page p in _pages)
{
foreach (Page p in _pages)
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
}
}
</select>
</select>
}
</div>
</div>
</div>
@@ -161,6 +163,7 @@
private string _pane;
private string _containerType;
private string _allPages = "false";
private bool _isShared = false;
private string _header = "";
private string _footer = "";
private string _permissionNames = "";
@@ -207,6 +210,7 @@
_expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate);
_allPages = pagemodule.Module.AllPages.ToString();
_isShared = pagemodule.Module.IsShared;
createdby = pagemodule.Module.CreatedBy;
createdon = pagemodule.Module.CreatedOn;
modifiedby = pagemodule.Module.ModifiedBy;
@@ -276,15 +280,40 @@
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (!string.IsNullOrEmpty(_title))
{
if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate))
{
AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning);
return;
}
}
// update module settings first
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy approach - module settings updated by convention (ie. by calling a public method named "UpdateSettings" in settings component)
// this method should be removed however the ISettingsControl declaration was not added to the default module template until version 10.1.2
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
// update container settings
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
// update page module
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
var pageId = pagemodule.PageId; // preserve
var pane = pagemodule.Pane; // preserve
pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title;
pagemodule.Pane = _pane;
@@ -302,33 +331,21 @@
pagemodule.Header = _header;
pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
// update page module order if page or pane changed
if (pageId != pagemodule.PageId || pane != pagemodule.Pane)
{
await PageModuleService.UpdatePageModuleOrderAsync(pageId, pane); // old page/pane
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); // new page/pane
}
// update module
var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList();
await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component )
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
else

View File

@@ -46,7 +46,7 @@
<Label Class="col-sm-3" For="move" HelpText="Select the location where you would like the page to be moved in relation to other pages" ResourceKey="Move">Move: </Label>
<div class="col-sm-9">
<select id="move" class="form-select" @bind="@_insert" required>
@if (_parentid == _currentparentid)
@if (_parentid == _currentparentid && !_copy)
{
<option value="=">&lt;@Localizer["ThisLocation.Keep"]&gt;</option>
}
@@ -225,7 +225,10 @@
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
@if (!_copy)
{
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
@@ -241,36 +244,40 @@
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div>
</TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules">
<Header>
@if (!_copy)
{
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td>
</Row>
</Pager>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td>
</Row>
</Pager>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
}
}
</TabStrip>
}
@@ -349,6 +356,7 @@
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private int _pageId;
private bool _copy = false;
private string _name;
private string _currentparentid;
private string _parentid = "-1";
@@ -394,6 +402,10 @@
{
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_pageId = Int32.Parse(PageState.QueryString["id"]);
if (PageState.QueryString.ContainsKey("copy"))
{
_copy = bool.Parse(PageState.QueryString["copy"]);
}
_page = await PageService.GetPageAsync(_pageId);
_icons = await SystemService.GetIconsAsync();
_iconresources = Utilities.GetFullTypeName(typeof(IconResources).AssemblyQualifiedName);
@@ -413,7 +425,7 @@
_children = new List<Page>();
foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid, CultureInfo.InvariantCulture))))
{
if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
if ((p.PageId != _pageId || _copy) && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
@@ -440,6 +452,12 @@
_expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate);
_ispersonalizable = _page.IsPersonalizable.ToString();
if (_copy)
{
_insert = ">";
_childid = _page.PageId;
}
// appearance
_title = _page.Title;
_themetype = _page.ThemeType;
@@ -470,6 +488,19 @@
// permissions
_permissions = _page.PermissionList;
_updatemodulepermissions = "True";
if (_copy)
{
_permissions = _page.PermissionList.Select(item => new Permission
{
SiteId = item.SiteId,
EntityName = item.EntityName,
EntityId = -1,
PermissionName = item.PermissionName,
RoleName = item.RoleName,
UserId = item.UserId,
IsAuthorized = item.IsAuthorized,
}).ToList();
}
// page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@@ -484,6 +515,13 @@
_deletedon = _page.DeletedOn;
ThemeSettings();
if (_copy)
{
_name = "";
_path = "";
}
_initialized = true;
}
else
@@ -554,7 +592,7 @@
builder.OpenComponent(0, _themeSettingsType);
builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary);
builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
@@ -576,10 +614,18 @@
await ScrollToPageTop();
return;
}
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
{
string currentPath = _page.Path;
if (_copy)
{
_page = new Page();
_page.SiteId = PageState.Site.SiteId;
currentPath = "";
}
_page.Name = _name;
if (_parentid == "-1")
@@ -635,6 +681,13 @@
return;
}
// update theme settings
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
}
// default page properties
if (_insert != "=")
{
Page child;
@@ -688,7 +741,21 @@
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
}
_page = await PageService.UpdatePageAsync(_page);
if (_copy)
{
// create page
_page = await PageService.AddPageAsync(_page);
await PageService.CopyPageAsync(_pageId, _page.PageId, bool.Parse(_updatemodulepermissions));
await logger.LogInformation("Page Added {Page}", _page);
}
else
{
// update page
_page = await PageService.UpdatePageAsync(_page);
await logger.LogInformation("Page Saved {Page}", _page);
}
// update page order
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId);
if (_currentparentid == string.Empty)
{
@@ -699,12 +766,6 @@
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid));
}
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
}
await logger.LogInformation("Page Saved {Page}", _page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{
NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload

View File

@@ -3,10 +3,11 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_initialized)
{
@@ -71,6 +72,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="Your preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
@@ -95,6 +108,8 @@
@code {
private bool _initialized = false;
private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Language> _languages;
private string _passwordrequirements;
private string _username = string.Empty;
private ElementReference form;
@@ -106,6 +121,7 @@
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private bool _userCreated = false;
private bool _allowsitelogin = true;
@@ -113,10 +129,13 @@
protected override async Task OnInitializedAsync()
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_culturecode = PageState.Site.CultureCode;
_initialized = true;
}
@@ -147,6 +166,7 @@
Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
CultureCode = _culturecode,
PhotoFileId = null
};
user = await UserService.AddUserAsync(user);

View File

@@ -11,11 +11,15 @@
@inject IThemeService ThemeService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject ILocalizationService LocalizationService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService
@inject IJobService JobService
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService
@inject ISiteGroupService SiteGroupService
@inject ISiteGroupMemberService SiteGroupMemberService
@if (_initialized)
{
@@ -28,22 +32,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="homepage" HelpText="Select the home page for the site (to be used if there is no page with a path of '/')" ResourceKey="HomePage">Home Page: </Label>
<div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (Page page in _pages)
{
if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<Label Class="col-sm-3" For="timezone" HelpText="The default time zone for the site" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@@ -54,6 +43,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The default language of the site's content" ResourceKey="Culture">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var culture in _cultures)
{
<option value="@culture.Name">@culture.DisplayName</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
@@ -207,13 +208,6 @@
</div>
@if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
@@ -264,15 +258,6 @@
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
@@ -348,57 +333,6 @@
</Section>
@if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@((context.IsDefault) ? SharedLocalizer["Yes"] : SharedLocalizer["No"])</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
</div>
</div>
</Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
<div class="container">
<div class="row mb-1 align-items-center">
@@ -424,7 +358,7 @@
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The render mode for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<Label Class="col-sm-3" For="runtime" HelpText="The hosting model for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option>
@@ -453,6 +387,180 @@
</div>
</div>
</Section>
<Section Name="Aliases" Heading="Urls" ResourceKey="Aliases">
<div class="container">
@if (!_addAlias)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="aliases" class="form-select" value="@_aliasid" @onchange="(e => AliasChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var alias in _aliases)
{
<option value="@alias.AliasId">@alias.Name @((alias.IsDefault) ? "(" + Localizer["Default"] + ")" : "")</option>
}
</select>
@if (!_addAlias)
{
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
}
</div>
</div>
</div>
}
@if (_aliasid != -1 || _addAlias)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliasname" HelpText="A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="AliasName">Url: </Label>
<div class="col-sm-9">
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultalias" HelpText="The default alias for the site. Requests for non-default aliases will be redirected to the default alias." ResourceKey="DefaultAlias">Default? </Label>
<div class="col-sm-9">
<select id="defaultalias" class="form-select" @bind="@_defaultalias">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<div class="col-sm-3"></div>
<div class="col-sm-9">
@if (_aliasid != -1 || _addAlias)
{
<button type="button" class="btn btn-success me-2" @onclick="SaveAlias">@SharedLocalizer["Save"]</button>
}
@if (_aliasid != -1 && !_addAlias)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias())" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", _aliasname])" />
}
@if (_addAlias)
{
<button type="button" class="btn btn-secondary" @onclick="CancelAlias">@SharedLocalizer["Cancel"]</button>
}
</div>
</div>
</div>
</Section>
<Section Name="SiteGroupMembers" Heading="Site Groups" ResourceKey="SiteGroupMembers">
<div class="container">
@if (!_addSiteGroup)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="group" HelpText="The site groups in this tenant (database)" ResourceKey="SiteGroupMembers">Group: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="group" class="form-select" value="@_siteGroupId" @onchange="(e => SiteGroupChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var siteGroup in _siteGroups)
{
<option value="@siteGroup.SiteGroupId">@siteGroup.Name</option>
}
</select>
@if (!_addSiteGroup)
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroup">@SharedLocalizer["Add"]</button>
}
</div>
</div>
</div>
}
@if (_siteGroupId != -1 || _addSiteGroup)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="groupname" HelpText="Name of the site group" ResourceKey="GroupName">Name: </Label>
<div class="col-sm-9">
<input id="groupname" class="form-control" @bind="@_groupName" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="grouptype" HelpText="Defines the specific behavior of the site group" ResourceKey="GroupType">Type: </Label>
<div class="col-sm-9">
<select id="grouptype" class="form-select" @bind="@_groupType">
<option value="@SiteGroupTypes.Synchronization">@Localizer[@SiteGroupTypes.Synchronization]</option>
<option value="@SiteGroupTypes.ChangeDetection">@Localizer[@SiteGroupTypes.ChangeDetection]</option>
<option value="@SiteGroupTypes.Localization">@Localizer[SiteGroupTypes.Localization]</option>
</select>
</div>
</div>
}
@if (_siteGroupId != -1)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="site" HelpText="The sites which are members of this site group" ResourceKey="Site">Members: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="site" class="form-select" value="@_siteId" @onchange="(e => SiteChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var site in _sites)
{
<option value="@site.SiteId">@site.Name</option>
}
</select>
@if (!_addSiteGroupMember)
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroupMember">@SharedLocalizer["Add"]</button>
}
else
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroupMember">@SharedLocalizer["Select"]</button>
}
</div>
</div>
</div>
}
@if (_siteGroupId != -1 && _siteId != -1)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="primary" HelpText="Indicates if the selected member is the primary site of the site group" ResourceKey="Primary">Primary? </Label>
<div class="col-sm-9">
<select id="primary" class="form-select" @bind="@_primary">
<option value="False">@SharedLocalizer["No"]</option>
<option value="True">@SharedLocalizer["Yes"]</option>
</select>
</div>
</div>
@if (_primary == "False" && !string.IsNullOrEmpty(_synchronized) && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronized" HelpText="The date/time when the site was last synchronized" ResourceKey="Synchronized">Synchronized: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="synchronized" class="form-control" @bind="@_synchronized" disabled />
@if (!string.IsNullOrEmpty(_synchronized))
{
<button type="button" class="btn btn-primary" @onclick="ResetSiteGroupMember">@SharedLocalizer["Reset"]</button>
}
</div>
</div>
</div>
}
}
<div class="row mb-1 align-items-center">
<div class="col-sm-3"></div>
<div class="col-sm-9">
@if ((_siteGroupId != -1 || _addSiteGroup))
{
<button type="button" class="btn btn-success me-2" @onclick="SaveSiteGroupMember">@SharedLocalizer["Save"]</button>
}
@if (_siteGroupId != -1 && !_addSiteGroup && _siteId != -1 && !_addSiteGroupMember)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteSiteGroupMember())" ResourceKey="DeleteSiteGroupMember" Class="btn btn-danger" Header="Delete Site Group" Message="@Localizer["Confirm.SiteGroupMember.Delete"]" />
}
@if (_addSiteGroup)
{
<button type="button" class="btn btn-secondary" @onclick="CancelSiteGroupMember">@SharedLocalizer["Cancel"]</button>
}
</div>
</div>
</div>
</Section>
<Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation">
<div class="container">
<div class="row mb-1 align-items-center">
@@ -493,10 +601,11 @@
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Culture> _cultures;
private string _name = string.Empty;
private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _isdeleted;
private string _sitemap = "";
private string _siteguid = "";
@@ -534,7 +643,6 @@
private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private int _retention = 30;
private string _pwaisenabled;
@@ -543,17 +651,29 @@
private int _pwasplashiconfileid = -1;
private FileManager _pwasplashiconfilemanager;
private List<Alias> _aliases;
private int _aliasid = -1;
private string _aliasname;
private string _defaultalias;
private string _rendermode = RenderModes.Interactive;
private string _enhancednavigation = "True";
private string _runtime = Runtimes.Server;
private string _prerender = "True";
private string _hybrid = "False";
private List<Alias> _aliases;
private int _aliasid = -1;
private string _aliasname;
private string _defaultalias;
private bool _addAlias = false;
private List<SiteGroup> _siteGroups = new List<SiteGroup>();
private List<Site> _sites = new List<Site>();
private int _siteGroupId = -1;
private int _siteId;
private string _groupName = string.Empty;
private string _groupType = SiteGroupTypes.Synchronization;
private string _primary = "True";
private string _synchronized = string.Empty;
private bool _addSiteGroup = false;
private bool _addSiteGroupMember = false;
private string _tenant = string.Empty;
private string _database = string.Empty;
private string _connectionstring = string.Empty;
@@ -580,16 +700,14 @@
if (site != null)
{
_timezones = TimeZoneService.GetTimeZones();
_cultures = await LocalizationService.GetNeutralCulturesAsync();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null)
{
_homepageid = site.HomePageId.Value.ToString();
}
_culturecode = site.CultureCode;
_isdeleted = site.IsDeleted.ToString();
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml";
_siteguid = site.SiteGuid;
@@ -653,7 +771,6 @@
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
}
@@ -669,7 +786,7 @@
}
// aliases
await GetAliases();
await LoadAliases();
// hosting model
_rendermode = site.RenderMode;
@@ -693,6 +810,12 @@
}
}
// site groups
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await LoadSiteGroups();
}
// audit
_createdby = site.CreatedBy;
_createdon = site.CreatedOn;
@@ -760,7 +883,7 @@
{
site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.CultureCode = _culturecode;
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
// appearance
@@ -846,8 +969,18 @@
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
if (_smtpenabled == "True")
{
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
}
//cookie consent
@@ -957,7 +1090,10 @@
{
try
{
_smtpenabled = "True";
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
@@ -972,6 +1108,14 @@
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
await ScrollToPageTop();
@@ -988,95 +1132,100 @@
}
}
private async Task GetAliases()
private async Task LoadAliases()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList();
_aliasid = -1;
_addAlias = false;
}
private async void AliasChanged(ChangeEventArgs e)
{
_aliasid = int.Parse(e.Value.ToString());
if (_aliasid != -1)
{
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList();
var alias = _aliases.FirstOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
}
}
else
{
_aliasname = "";
_defaultalias = "False";
}
StateHasChanged();
}
private void AddAlias()
{
_aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false });
_aliasid = 0;
_aliasid = -1;
_aliasname = "";
_defaultalias = "False";
_addAlias = true;
StateHasChanged();
}
private void EditAlias(Alias alias)
private async Task DeleteAlias()
{
_aliasid = alias.AliasId;
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
await AliasService.DeleteAliasAsync(_aliasid);
await LoadAliases();
StateHasChanged();
}
private async Task DeleteAlias(Alias alias)
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
await GetAliases();
StateHasChanged();
}
}
private async Task SaveAlias()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
if (!string.IsNullOrEmpty(_aliasname))
{
if (!string.IsNullOrEmpty(_aliasname))
var aliases = await AliasService.GetAliasesAsync();
int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase);
if (protocolIndex != -1)
{
var aliases = await AliasService.GetAliasesAsync();
_aliasname = _aliasname.Substring(protocolIndex + 3);
}
int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase);
if (protocolIndex != -1)
var alias = aliases.FirstOrDefault(item => item.Name == _aliasname);
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
{
if (_aliasid == -1)
{
_aliasname = _aliasname.Substring(protocolIndex + 3);
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
var alias = aliases.FirstOrDefault(item => item.Name == _aliasname);
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
else
{
if (_aliasid == 0)
alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
else
{
alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
await GetAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
else // Duplicate alias
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
await ScrollToPageTop();
}
await LoadAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
else // Duplicate alias
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
await ScrollToPageTop();
}
}
}
private async Task CancelAlias()
{
await GetAliases();
await LoadAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
@@ -1086,4 +1235,205 @@
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
}
private async Task LoadSiteGroups()
{
_siteGroups = await SiteGroupService.GetSiteGroupsAsync();
_siteGroupId = -1;
_addSiteGroup = false;
StateHasChanged();
}
private async Task SiteGroupChanged(ChangeEventArgs e)
{
_siteGroupId = int.Parse(e.Value.ToString());
if (_siteGroupId != -1)
{
var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
if (group != null)
{
_groupName = group.Name;
_groupType = group.Type;
_siteId = -1;
_primary = "False";
_addSiteGroupMember = false;
await LoadSites();
}
}
StateHasChanged();
}
private async Task LoadSites()
{
var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId);
_sites = await SiteService.GetSitesAsync();
if (_addSiteGroupMember)
{
// include sites which are not members
_sites = _sites.ExceptBy(siteGroupMembers.Select(item => item.SiteId), item => item.SiteId).ToList();
}
else
{
// include sites which are members
_sites = _sites.Where(item => siteGroupMembers.Any(item2 => item2.SiteId == item.SiteId)).ToList();
var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
foreach (var site in _sites)
{
if (group.PrimarySiteId == site.SiteId)
{
site.Name += $" ({Localizer["Primary"]})";
}
else
{
site.Name += $" ({Localizer["Secondary"]})";
}
}
var siteGroupMember = siteGroupMembers.FirstOrDefault(item => item.SiteId == _siteId);
if (siteGroupMember != null)
{
_primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False";
_synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString();
}
}
}
private async Task SiteChanged(ChangeEventArgs e)
{
_siteId = int.Parse(e.Value.ToString());
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId);
if (siteGroupMember != null)
{
_primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False";
_synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString();
}
StateHasChanged();
}
private async Task AddSiteGroup()
{
_groupName = "";
_siteId = PageState.Site.SiteId;
_primary = "True";
_synchronized = "";
_addSiteGroup = true;
}
private async Task AddSiteGroupMember()
{
_addSiteGroupMember = !_addSiteGroupMember;
_siteId = -1;
await LoadSites();
}
private async Task SaveSiteGroupMember()
{
if (string.IsNullOrEmpty(_groupName))
{
AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning);
await ScrollToPageTop();
return;
}
SiteGroup siteGroup = null;
if (_siteGroupId == -1)
{
siteGroup = new SiteGroup
{
Name = _groupName,
Type = _groupType,
PrimarySiteId = _siteId,
Synchronize = false
};
siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup);
}
else
{
siteGroup = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
if (siteGroup != null)
{
siteGroup.Name = _groupName;
siteGroup.Type = _groupType;
siteGroup.PrimarySiteId = (_primary == "True") ? _siteId : siteGroup.PrimarySiteId;
siteGroup = await SiteGroupService.UpdateSiteGroupAsync(siteGroup);
}
else
{
siteGroup = null;
}
}
if (siteGroup != null)
{
if (_siteId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId);
if (siteGroupMember == null)
{
siteGroupMember = new SiteGroupMember
{
SiteGroupId = siteGroup.SiteGroupId,
SiteId = _siteId
};
await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember);
}
else
{
siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn;
await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember);
}
}
if (siteGroup.Type == SiteGroupTypes.Synchronization)
{
// enable synchronization job if it is not enabled already
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
await LoadSiteGroups();
}
}
private async Task CancelSiteGroupMember()
{
_groupName = "";
await LoadSiteGroups();
}
private async Task DeleteSiteGroupMember()
{
if (_siteGroupId != -1)
{
if (_siteId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId);
if (siteGroupMember != null)
{
await SiteGroupMemberService.DeleteSiteGroupMemberAsync(siteGroupMember.SiteGroupMemberId);
}
}
var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId);
if (!siteGroupMembers.Any())
{
await SiteGroupService.DeleteSiteGroupAsync(_siteGroupId);
}
await LoadSiteGroups();
}
}
private async Task ResetSiteGroupMember()
{
_synchronized = "";
}
}

View File

@@ -6,7 +6,6 @@
@inject ITenantService TenantService
@inject IAliasService AliasService
@inject ISiteService SiteService
@inject IThemeService ThemeService
@inject ISiteTemplateService SiteTemplateService
@inject IUserService UserService
@inject IInstallationService InstallationService
@@ -29,33 +28,9 @@ else
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label>
<Label Class="col-sm-3" For="alias" HelpText="The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder)." ResourceKey="Aliases">Url: </Label>
<div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the default theme for the website" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
<select id="defaultTheme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
<option value="-">&lt;@Localizer["Theme.Select"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
<div class="col-sm-9">
<select id="defaultContainer" class="form-select" @bind="@_containertype" required>
<option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
<input id="alias" class="form-control" @bind="@_urls" required></input>
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -70,26 +45,6 @@ else
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="rendermode" HelpText="The default render mode for the site" ResourceKey="Rendermode">Render Mode: </Label>
<div class="col-sm-9">
<select id="rendermode" class="form-select" @bind="@_rendermode" required>
<option value="@RenderModes.Interactive">@(SharedLocalizer["RenderMode" + @RenderModes.Interactive])</option>
<option value="@RenderModes.Static">@(SharedLocalizer["RenderMode" + @RenderModes.Static])</option>
<option value="@RenderModes.Headless">@(SharedLocalizer["RenderMode" + @RenderModes.Headless])</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The render mode for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option>
<option value="@Runtimes.WebAssembly">@(SharedLocalizer["Runtime" + @Runtimes.WebAssembly])</option>
<option value="@Runtimes.Auto">@(SharedLocalizer["Runtime" + @Runtimes.Auto])</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the database for the site" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9">
@@ -186,9 +141,6 @@ else
private bool _showConnectionString = false;
private string _connectionString = string.Empty;
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants;
private string _tenantid = "-";
@@ -200,11 +152,7 @@ else
private string _name = string.Empty;
private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _sitetemplatetype = "-";
private string _rendermode = RenderModes.Static;
private string _runtime = Runtimes.Server;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -215,19 +163,11 @@ else
{
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
}
_urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{
_themetype = Constants.DefaultTheme;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = _containers.First().TypeName;
}
_urls = PageState.Alias.Name + "/sitename";
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
if (_siteTemplates.Any(item => item.TypeName == Constants.DefaultSiteTemplate))
if (_siteTemplates.Any(item => item.TypeName == Constants.EmptySiteTemplate))
{
_sitetemplatetype = Constants.DefaultSiteTemplate;
_sitetemplatetype = Constants.EmptySiteTemplate;
}
_databases = await DatabaseService.GetDatabasesAsync();
@@ -281,37 +221,13 @@ else
StateHasChanged();
}
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = _containers.First().TypeName;
}
else
{
_containers = new List<ThemeControl>();
_containertype = "-";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error);
}
}
private async Task SaveSite()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-")
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _sitetemplatetype != "-")
{
_urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>();
@@ -399,12 +315,12 @@ else
{
config.SiteName = _name;
config.Aliases = _urls;
config.DefaultTheme = _themetype;
config.DefaultContainer = _containertype;
config.DefaultTheme = Constants.DefaultTheme;
config.DefaultContainer = Constants.DefaultContainer;
config.DefaultAdminContainer = "";
config.SiteTemplate = _sitetemplatetype;
config.RenderMode = _rendermode;
config.Runtime = _runtime;
config.RenderMode = RenderModes.Static;
config.Runtime = Runtimes.Server;
config.Register = false;
ShowProgressIndicator();

View File

@@ -83,14 +83,14 @@ else
{
@if (_connection != "-")
{
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
</div>
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9">
@@ -150,130 +150,149 @@ else
}
@code {
private string _connection = "-";
private Dictionary<string, object> _connections;
private List<Tenant> _tenants;
private List<Database> _databases;
private string _connection = "-";
private Dictionary<string, object> _connections;
private List<Tenant> _tenants;
private List<Database> _databases;
private string _name = string.Empty;
private string _databasetype = string.Empty;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _tenant = string.Empty;
private string _connectionstring = string.Empty;
private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty;
private List<Dictionary<string, string>> _results;
private string _name = string.Empty;
private string _databasetype = string.Empty;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _tenant = string.Empty;
private string _connectionstring = string.Empty;
private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty;
private List<Dictionary<string, string>> _results;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
protected override async Task OnInitializedAsync()
{
try
{
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private async void ConnectionChanged(ChangeEventArgs e)
{
try
{
_connection = (string)e.Value;
if (_connection != "-" && _connection != "+")
{
_connectionstring = _connections[_connection].ToString();
_tenant = "";
_databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null)
{
_tenant = tenant.Name;
private async void ConnectionChanged(ChangeEventArgs e)
{
try
{
_connection = (string)e.Value;
if (_connection != "-" && _connection != "+")
{
_connectionstring = _connections[_connection].ToString();
_tenant = "";
_databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null)
{
_tenant = tenant.Name;
// hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order
_databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name;
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
}
else
{
if (_connection.Contains(" ("))
{
_databasetype = _connection.Substring(_connection.LastIndexOf(" (") + 2).Replace(")", "");
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName;
}
}
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName;
}
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{
try
{
_databasetype = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{
try
{
_databasetype = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
if (database != null)
{
_databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder =>
{
builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent();
};
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
if (database != null)
{
_databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder =>
{
builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent();
};
}
}
private void ShowConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionstring = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
private void ShowConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionstring = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
private async Task Add()
{
var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionstring = databaseConfigControl.GetConnectionString();
}
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{
var settings = new Dictionary<string, object>();
private async Task Add()
{
var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionstring = databaseConfigControl.GetConnectionString();
}
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{
_name = _name + " (" + _databasetype +")";
var settings = new Dictionary<string, object>();
settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring);
await SystemService.UpdateSystemInfoAsync(settings);
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");

View File

@@ -30,6 +30,7 @@
</div>
</div>
</form>
<br />
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
@@ -81,6 +82,12 @@
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="fingerprint" HelpText="A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting)." ResourceKey="Fingerprint">Fingerprint: </Label>
<div class="col-sm-9">
<input id="fingerprint" class="form-control" @bind="@_fingerprint" disabled />
</div>
</div>
</div>
</Section>
<br />
@@ -117,6 +124,7 @@
private string _url = "";
private string _contact = "";
private string _license = "";
private string _fingerprint = "";
private List<Permission> _permissions = null;
private string _createdby;
private DateTime _createdon;
@@ -143,6 +151,7 @@
_url = theme.Url;
_contact = theme.Contact;
_license = theme.License;
_fingerprint = theme.Fingerprint;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy;
_createdon = theme.CreatedOn;

View File

@@ -10,6 +10,7 @@
@inject IFileService FileService
@inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@@ -58,6 +59,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="Your preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
@@ -448,6 +461,9 @@
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private List<Models.TimeZone> _timezones;
private IEnumerable<Language> _languages;
private bool _initialized = false;
private bool _allowtwofactor = false;
private bool _allowpasskeys = false;
@@ -458,8 +474,8 @@
private string _displayname = string.Empty;
private FileManager _filemanager;
private int _folderid = -1;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private int _photofileid = -1;
private File _photo = null;
private string _imagefiles = string.Empty;
@@ -493,12 +509,15 @@
if (PageState.User != null)
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
// identity section
_username = PageState.User.Username;
_email = PageState.User.Email;
_displayname = PageState.User.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
_culturecode = PageState.User.CultureCode;
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
{
@@ -572,6 +591,7 @@
user.Email = _email;
user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1)
{

View File

@@ -6,6 +6,7 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -55,6 +56,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The user's preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9">
@@ -129,12 +142,15 @@
@code {
private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Language> _languages;
private bool _initialized = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _notify = "True";
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
@@ -147,6 +163,11 @@
try
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_timezoneid = PageState.Site.TimeZoneId;
_culturecode = PageState.Site.CultureCode;
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles)
{
@@ -157,8 +178,9 @@
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}
catch (Exception ex)
@@ -194,6 +216,7 @@
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);

View File

@@ -7,6 +7,7 @@
@inject ISettingService SettingService
@inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -55,6 +56,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The user's preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
@@ -211,7 +224,7 @@
{
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True")
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _candelete)
{
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
@@ -224,17 +237,21 @@
private bool _allowpasskeys = false;
private bool _allowexternallogin = false;
private List<Models.TimeZone> _timezones;
private IEnumerable<Language> _languages;
private int _userid;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _isdeleted;
private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private bool _candelete = false;
private string _passwordrequirements;
private string _password = string.Empty;
@@ -270,13 +287,17 @@
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null)
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_username = user.Username;
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
_culturecode = PageState.User.CultureCode;
_isdeleted = user.IsDeleted.ToString();
_candelete = user.IsDeleted;
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
@@ -344,6 +365,7 @@
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));

View File

@@ -1,7 +1,7 @@
@namespace Oqtane.Modules.Admin.Users
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject ISiteTaskService SiteTaskService
@inject IStringLocalizer<Users> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -43,17 +43,9 @@
var fileid = _filemanager.GetFileId();
if (fileid != -1)
{
ShowProgressIndicator();
var results = await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid, bool.Parse(_notify));
if (bool.Parse(results["Success"]))
{
AddModuleMessage(string.Format(Localizer["Message.Import.Success"], results["Users"]), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error);
}
HideProgressIndicator();
var siteTask = new SiteTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}");
await SiteTaskService.AddSiteTaskAsync(siteTask);
AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success);
}
else
{

View File

@@ -61,6 +61,12 @@
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" />
}
@if (MaxUploadFileSize > 0)
{
<div class="row my-1">
<small class="fw-light">@string.Format(Localizer["File.MaxSize"], MaxUploadFileSize)</small>
</div>
}
</div>
<div class="col-auto">
<button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@@ -163,6 +169,9 @@
[Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter]
public int MaxUploadFileSize { get; set; } = -1; // optional - maximum upload file size in MB
[Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
@@ -381,16 +390,39 @@
if (uploads.Length > 0)
{
string restricted = "";
string tooLarge = "";
foreach (var upload in uploads)
{
var filename = upload.Split(':')[0];
var fileparts = upload.Split(':');
var filename = fileparts[0];
if (MaxUploadFileSize > 0)
{
var filesizeBytes = long.Parse(fileparts[1]);
var filesizeMB = (double)filesizeBytes / (1024 * 1024);
if (filesizeMB > MaxUploadFileSize)
{
tooLarge += (tooLarge == "" ? "" : ",") + filename;
}
}
var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : "";
if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower()))
{
restricted += (restricted == "" ? "" : ",") + extension;
}
}
if (restricted == "")
if (restricted != "")
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
else if (tooLarge != "")
{
_message = string.Format(Localizer["Message.File.TooLarge"], tooLarge, MaxUploadFileSize);
_messagetype = MessageType.Warning;
}
else
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
@@ -490,11 +522,6 @@
tokenSource.Dispose();
}
}
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
}
else
{

View File

@@ -21,7 +21,7 @@
@if (_style == MessageStyle.Toast)
{
<div class="app-modulemessage-toast bottom-0 end-0" @key="DateTime.UtcNow">
<div class="app-modulemessage-toast bottom-0 end-0">
<div class="@_classname alert-dismissible fade show mb-3 rounded-end-0" role="alert">
@((MarkupString)Message)
@if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@@ -86,11 +86,19 @@
/// <summary>
/// Determines if a tab should be visible based on user permissions.
/// Authorization hierarchy:
/// 1. Host and Admin roles ALWAYS have access (bypass all checks)
/// 2. Check standard SecurityAccessLevel (View, Edit, etc.)
/// 3. If RoleName specified AND user is not Admin/Host, check RoleName
/// 4. If PermissionName specified AND user is not Admin/Host, check PermissionName
/// Authorization follows this hierarchy:
/// 1. Host tabs (Security == Host): Only users with Host role can access (Admins excluded)
/// 2. Admin users: Bypass all other checks (except Host restrictions)
/// 3. SecurityAccessLevel check (null/Anonymous/View/Edit/Host):
/// - null: No security level restriction (proceeds to step 4)
/// - Anonymous: No authentication required
/// - View/Edit: Requires corresponding module permission
/// - Host: Only Host role can access
/// 4. Additional RoleName requirement (if specified)
/// 5. Additional PermissionName requirement (if specified)
///
/// Important: When Security is null, RoleName and PermissionName checks STILL apply
/// (Security = null doesn't mean unrestricted, it means "no security level required")
/// </summary>
/// <param name="tabPanel">The tab panel to check authorization for</param>
/// <returns>True if user is authorized to see this tab, false otherwise</returns>
@@ -99,24 +107,40 @@
// Step 1: Check for Host-only restriction
if (tabPanel.Security == SecurityAccessLevel.Host)
{
// Only Host users can access Host-level security tabs (Admin users are excluded)
return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
}
// Step 2: Admin bypass all other restrictions
// Step 2: Admin bypass all restrictions except Host
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
var authorized = false;
// Step 3: If Security is null, check only RoleName and PermissionName
if (tabPanel.Security == null)
{
// Start with authorized = true for null security
bool isAuthorized = true;
// Step 3: Check standard SecurityAccessLevel
// Only apply RoleName check if provided
if (!string.IsNullOrEmpty(tabPanel.RoleName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Only apply PermissionName check if provided
if (isAuthorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);
}
return isAuthorized;
}
// Handle other SecurityAccessLevel values
bool authorized = false; // Use different variable name or move declaration
switch (tabPanel.Security)
{
case null:
authorized = true;
break;
case SecurityAccessLevel.Anonymous:
authorized = true;
break;
@@ -131,13 +155,13 @@
break;
}
// Step 4: Check RoleName if provided (additional requirement)
// Step 4: Additional RoleName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName))
{
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Step 5: Check PermissionName if provided (additional requirement)
// Step 5: Additional PermissionName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);

View File

@@ -31,8 +31,8 @@
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["CreatedOn"]</th>
<th>@SharedLocalizer["CreatedBy"]</th>
<th>@Localizer["CreatedOn"]</th>
<th>@Localizer["CreatedBy"]</th>
</Header>
<Row>
<td><ActionLink Action="View" Security="SecurityAccessLevel.Edit" OnClick="@(async () => await View(context))" ResourceKey="View" /></td>
@@ -122,13 +122,9 @@
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
if (htmltext != null)
{
_view = htmltext.Content;
_view = Utilities.FormatContent(_view, PageState.Alias, "render");
StateHasChanged();
}
_view = htmltext.Content;
_view = Utilities.FormatContent(_view, PageState.Alias, "render");
StateHasChanged();
}
catch (Exception ex)
{
@@ -141,19 +137,15 @@
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId);
if (htmltext != null)
{
var content = htmltext.Content;
htmltext = new HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
await logger.LogInformation("Content Restored {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
var content = htmltext.Content;
htmltext = new HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
await logger.LogInformation("Content Restored {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
catch (Exception ex)
{
@@ -166,15 +158,11 @@
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId);
if (htmltext != null)
{
await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
await logger.LogInformation("Content Deleted {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
await logger.LogInformation("Content Deleted {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
catch (Exception ex)
{

View File

@@ -7,6 +7,18 @@ using Oqtane.Shared;
namespace Oqtane.Modules.HtmlText.Services
{
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public interface IHtmlTextService
{
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService
{
@@ -24,11 +36,6 @@ namespace Oqtane.Modules.HtmlText.Services
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
{
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText)
{
return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText);

View File

@@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
namespace Oqtane.Modules.HtmlText.Services
{
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public interface IHtmlTextService
{
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
}

View File

@@ -16,6 +16,12 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="versions" ResourceKey="Versions" ResourceType="@resourceType" HelpText="The number of content versions to preserve (note that zero means unlimited)">Versions: </Label>
<div class="col-sm-9">
<input id="versions" type="number" min="0" max="9" step="1" class="form-control" @bind="@_versions" />
</div>
</div>
</div>
</form>
@@ -26,12 +32,14 @@
private bool validated = false;
private string _dynamictokens;
private string _versions = "5";
protected override void OnInitialized()
{
try
{
_dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false");
_versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "5");
}
catch (Exception ex)
{
@@ -45,6 +53,10 @@
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens);
if (int.TryParse(_versions, out int versions) && versions >= 0 && versions <= 9)
{
settings = SettingService.SetSetting(settings, "Versions", versions.ToString());
}
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)

View File

@@ -8,11 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Radzen.Blazor" Version="8.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Radzen.Blazor" Version="10.0.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,174 @@
<?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="CaseSensitive.Text" xml:space="preserve">
<value>Match Case?</value>
</data>
<data name="CaseSensitive.HelpText" xml:space="preserve">
<value>Specify if the replacement operation should be case sensitive</value>
</data>
<data name="Content.Text" xml:space="preserve">
<value>Module Content?</value>
</data>
<data name="Content.HelpText" xml:space="preserve">
<value>Specify if module content should be updated</value>
</data>
<data name="Pages.Text" xml:space="preserve">
<value>Page Info?</value>
</data>
<data name="Pages.HelpText" xml:space="preserve">
<value>Specify if page information should be updated (ie. name, title, head content, body content settings)</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Site Info?</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>Specify if site information should be updated (ie. name, head content, body content, settings)</value>
</data>
<data name="Replace.Text" xml:space="preserve">
<value>Replace With:</value>
</data>
<data name="Replace.HelpText" xml:space="preserve">
<value>Specify the replacement content</value>
</data>
<data name="Modules.Text" xml:space="preserve">
<value>Module Info?</value>
</data>
<data name="Modules.HelpText" xml:space="preserve">
<value>Specify if module information should be updated (ie. title, header, footer settings)</value>
</data>
<data name="Success.Save" xml:space="preserve">
<value>Your Global Replace Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient.</value>
</data>
<data name="Error.Save" xml:space="preserve">
<value>Error Saving Global Replace</value>
</data>
<data name="Find.HelpText" xml:space="preserve">
<value>Specify the content which needs to be replaced</value>
</data>
<data name="Find.Text" xml:space="preserve">
<value>Find What:</value>
</data>
<data name="GlobalReplace.Header" xml:space="preserve">
<value>Global Replace</value>
</data>
<data name="GlobalReplace.Message" xml:space="preserve">
<value>This Operation is Permanent. Are You Sure You Wish To Proceed?</value>
</data>
</root>

View File

@@ -139,7 +139,7 @@
<value>Error Updating Job</value>
</data>
<data name="Message.Required.JobInfo" xml:space="preserve">
<value>You Must Provide The Job Name, Type, Frequency, and Retention</value>
<value>You Must Provide The Job Name, Frequency, and Retention</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the job name</value>
@@ -154,7 +154,7 @@
<value>Select how often you want the job to run</value>
</data>
<data name="Starting.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should start executing</value>
<value>Optionally enter the date and time when this job should start executing. If no date or time is specified, the job will execute immediately.</value>
</data>
<data name="Ending.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should stop executing</value>
@@ -163,7 +163,7 @@
<value>Number of log entries to retain for this job</value>
</data>
<data name="NextExecution.HelpText" xml:space="preserve">
<value>Optionally modify the date and time when this job should execute next</value>
<value>The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job.</value>
</data>
<data name="Type.Text" xml:space="preserve">
<value>Type: </value>
@@ -193,6 +193,15 @@
<value>Execute Once</value>
</data>
<data name="Message.StartEndDateError" xml:space="preserve">
<value>Start Date cannot be after End Date.</value>
<value>The Start Date Cannot Be Later Than The End Date</value>
</data>
<data name="Message.StartDateError" xml:space="preserve">
<value>The Start Date Cannot Be Prior To The Current Date</value>
</data>
<data name="Message.ExecutingError" xml:space="preserve">
<value>The Job Is Currently Executing. You Must Wait Until The Job Has Completed.</value>
</data>
<data name="JobNotEditable" xml:space="preserve">
<value>The Job Cannot Be Modified As It Is Currently Enabled. You Must Disable The Job To Change Its Settings.</value>
</data>
</root>

View File

@@ -234,4 +234,10 @@
<data name="Pages.Heading" xml:space="preserve">
<value>Pages</value>
</data>
<data name="Fingerprint.Text" xml:space="preserve">
<value>Fingerprint:</value>
</data>
<data name="Fingerprint.HelpText" xml:space="preserve">
<value>A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting).</value>
</data>
</root>

View File

@@ -130,7 +130,7 @@
<value>Indicate if this module should be displayed on all pages</value>
</data>
<data name="Page.HelpText" xml:space="preserve">
<value>The page that the module is located on</value>
<value>The page that the module is located on. Please note that shared modules cannot be moved to other pages.</value>
</data>
<data name="Title.Text" xml:space="preserve">
<value>Title: </value>

View File

@@ -186,4 +186,10 @@
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>Your preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root>

View File

@@ -132,9 +132,6 @@
<data name="DefaultAdminContainer" xml:space="preserve">
<value>Default Admin Container</value>
</data>
<data name="Smtp.Required.EnableNotificationJob" xml:space="preserve">
<value>** Please Note That SMTP Requires The Notification Job To Be Enabled In Scheduled Jobs</value>
</data>
<data name="Smtp.TestConfig" xml:space="preserve">
<value>Test SMTP Configuration</value>
</data>
@@ -322,16 +319,16 @@
<value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value>
</data>
<data name="DefaultAlias.Text" xml:space="preserve">
<value>Default Alias: </value>
<value>Default?</value>
</data>
<data name="Aliases.Heading" xml:space="preserve">
<value>Site Urls</value>
</data>
<data name="AliasName" xml:space="preserve">
<value>Url</value>
<data name="AliasName.Text" xml:space="preserve">
<value>Url:</value>
</data>
<data name="AliasDefault" xml:space="preserve">
<value>Default?</value>
<data name="Default" xml:space="preserve">
<value>Default</value>
</data>
<data name="Confirm.Alias.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
@@ -342,12 +339,6 @@
<data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value>
</data>
<data name="SmtpRelay.HelpText" xml:space="preserve">
<value>Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above.</value>
</data>
<data name="SmtpRelay.Text" xml:space="preserve">
<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. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value>
</data>
@@ -409,7 +400,7 @@
<value>Hybrid Enabled?</value>
</data>
<data name="Runtime.HelpText" xml:space="preserve">
<value>The render mode for UI components which require interactivity</value>
<value>The hosting model for UI components which require interactivity</value>
</data>
<data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value>
@@ -504,4 +495,76 @@
<data name="StartTlsWhenAvailable" xml:space="preserve">
<value>Use TLS When Available</value>
</data>
<data name="AliasName.HelpText" xml:space="preserve">
<value>A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder).</value>
</data>
<data name="SiteGroupMembers.Text" xml:space="preserve">
<value>Group:</value>
</data>
<data name="SiteGroupMembers.HelpText" xml:space="preserve">
<value>The site groups in this tenant (database)</value>
</data>
<data name="Primary.Text" xml:space="preserve">
<value>Primary?</value>
</data>
<data name="Primary.HelpText" xml:space="preserve">
<value>Indicates if the selected member is the primary site of the site group</value>
</data>
<data name="GroupName.Text" xml:space="preserve">
<value>Name:</value>
</data>
<data name="GroupName.HelpText" xml:space="preserve">
<value>Name of the site group</value>
</data>
<data name="Primary" xml:space="preserve">
<value>Primary</value>
</data>
<data name="Secondary" xml:space="preserve">
<value>Secondary</value>
</data>
<data name="Compare" xml:space="preserve">
<value>Compare</value>
</data>
<data name="Update" xml:space="preserve">
<value>Update</value>
</data>
<data name="DeleteSiteGroupMember.Header" xml:space="preserve">
<value>Delete Site Group Member</value>
</data>
<data name="Confirm.SiteGroupMember.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete This Member From The Site Group?</value>
</data>
<data name="Message.Required.GroupName" xml:space="preserve">
<value>Site Group Name Is Required</value>
</data>
<data name="Message.Site.Synchronize" xml:space="preserve">
<value>Site Submitted For Synchronization</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Members:</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>The sites which are members of this site group</value>
</data>
<data name="Synchronized.Text" xml:space="preserve">
<value>Synchronized:</value>
</data>
<data name="GroupType.Text" xml:space="preserve">
<value>Type:</value>
</data>
<data name="GroupType.HelpText" xml:space="preserve">
<value>Defines the specific behavior of the site group</value>
</data>
<data name="Synchronized.HelpText" xml:space="preserve">
<value>The date/time when the site was last synchronized</value>
</data>
<data name="Synchronization" xml:space="preserve">
<value>Synchronization</value>
</data>
<data name="Localization" xml:space="preserve">
<value>Localization</value>
</data>
<data name="ChangeDetection" xml:space="preserve">
<value>Change Detection</value>
</data>
</root>

View File

@@ -123,29 +123,14 @@
<data name="SqlServer" xml:space="preserve">
<value>SQL Server</value>
</data>
<data name="Container.Select" xml:space="preserve">
<value>Select Container</value>
</data>
<data name="DefaultContainer.Text" xml:space="preserve">
<value>Default Container: </value>
</data>
<data name="Theme.Select" xml:space="preserve">
<value>Select Theme</value>
</data>
<data name="Aliases.HelpText" xml:space="preserve">
<value>The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value>
</data>
<data name="DefaultContainer.HelpText" xml:space="preserve">
<value>Select the default container for the site</value>
<value>The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value>
</data>
<data name="Tenant.Text" xml:space="preserve">
<value>Database: </value>
</data>
<data name="Aliases.Text" xml:space="preserve">
<value>Urls: </value>
</data>
<data name="DefaultTheme.Text" xml:space="preserve">
<value>Default Theme: </value>
<value>Url: </value>
</data>
<data name="SiteTemplate.Select" xml:space="preserve">
<value>Select Site Template</value>
@@ -177,9 +162,6 @@
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the name of the site</value>
</data>
<data name="DefaultTheme.HelpText" xml:space="preserve">
<value>Select the default theme for the site</value>
</data>
<data name="SiteTemplate.HelpText" xml:space="preserve">
<value>Select the site template</value>
</data>
@@ -222,12 +204,6 @@
<data name="Error.Database.LoadConfig" xml:space="preserve">
<value>Error loading Database Configuration Control</value>
</data>
<data name="RenderMode.HelpText" xml:space="preserve">
<value>The default render mode for the site</value>
</data>
<data name="RenderMode.Text" xml:space="preserve">
<value>Render Mode: </value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value>
</data>
@@ -240,10 +216,4 @@
<data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value>
</data>
<data name="Runtime.HelpText" xml:space="preserve">
<value>The render mode for UI components which require interactivity</value>
</data>
<data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value>
</data>
</root>

View File

@@ -186,4 +186,10 @@
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="Fingerprint.Text" xml:space="preserve">
<value>Fingerprint:</value>
</data>
<data name="Fingerprint.HelpText" xml:space="preserve">
<value>A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting).</value>
</data>
</root>

View File

@@ -148,7 +148,7 @@
<value>You Cannot Perform A System Update In A Development Environment</value>
</data>
<data name="Disclaimer.Text" xml:space="preserve">
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process.</value>
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Enterprise Installations And Microsoft Azure Installations You Will Want To Use A Manual Upgrade Process.</value>
</data>
<data name="Backup.Text" xml:space="preserve">
<value>Backup Files?</value>

View File

@@ -288,4 +288,10 @@
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Could Not Be Created</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>Your preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root>

View File

@@ -168,4 +168,10 @@
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value />
</data>
</root>

View File

@@ -252,4 +252,10 @@
<data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>The user's preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root>

View File

@@ -129,11 +129,8 @@
<data name="Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="Message.Import.Failure" xml:space="preserve">
<value>User Import Failed. Please Review Your Event Log For More Detailed Information.</value>
</data>
<data name="Message.Import.Success" xml:space="preserve">
<value>User Import Successful. {0} Users Imported.</value>
<value>Your User Import Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient.</value>
</data>
<data name="Message.Import.Validation" xml:space="preserve">
<value>You Must Specify A User File For Import</value>

View File

@@ -144,4 +144,10 @@
<data name="Message.File.Restricted" xml:space="preserve">
<value>Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information.</value>
</data>
<data name="File.MaxSize" xml:space="preserve">
<value>Maximum upload file size: {0} MB</value>
</data>
<data name="Message.File.TooLarge" xml:space="preserve">
<value>File(s) {0} exceed(s) the maximum upload size of {1} MB</value>
</data>
</root>

View File

@@ -123,4 +123,10 @@
<data name="DynamicTokens.Text" xml:space="preserve">
<value>Dynamic Tokens?</value>
</data>
<data name="Versions.HelpText" xml:space="preserve">
<value>The number of content versions to preserve (note that zero means unlimited)</value>
</data>
<data name="Versions.Text" xml:space="preserve">
<value>Versions:</value>
</data>
</root>

View File

@@ -204,6 +204,9 @@
<data name="System Update" xml:space="preserve">
<value>System Update</value>
</data>
<data name="Setting Management" xml:space="preserve">
<value>Setting Management</value>
</data>
<data name="Download" xml:space="preserve">
<value>Download</value>
</data>
@@ -480,4 +483,7 @@
<data name="Installed" xml:space="preserve">
<value>Installed</value>
</data>
<data name="Global Replace" xml:space="preserve">
<value>Global Replace</value>
</data>
</root>

View File

@@ -174,9 +174,6 @@
<data name="Title" xml:space="preserve">
<value>Title:</value>
</data>
<data name="System.Update" xml:space="preserve">
<value>Check For System Updates</value>
</data>
<data name="Visibility" xml:space="preserve">
<value>Visibility:</value>
</data>
@@ -200,5 +197,11 @@
</data>
<data name="Module.CopyExisting" xml:space="preserve">
<value>Copy Existing Module</value>
</data>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Synchronize Site</value>
</data>
<data name="Copy" xml:space="preserve">
<value>Copy Page</value>
</data>
</root>

View File

@@ -14,8 +14,9 @@ namespace Oqtane.Services
/// Set the localization cookie
/// </summary>
/// <param name="culture"></param>
/// <param name="uiCulture"></param>
/// <returns></returns>
Task SetLocalizationCookieAsync(string culture);
Task SetLocalizationCookieAsync(string culture, string uiCulture);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -23,7 +24,7 @@ namespace Oqtane.Services
{
public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public Task SetLocalizationCookieAsync(string culture)
public Task SetLocalizationCookieAsync(string culture, string uiCulture)
{
return Task.CompletedTask; // only used in server side rendering
}

View File

@@ -13,10 +13,16 @@ namespace Oqtane.Services
public interface ILocalizationService
{
/// <summary>
/// Returns a collection of supported cultures
/// Returns a collection of supported or installed cultures
/// </summary>
/// <returns></returns>
Task<IEnumerable<Culture>> GetCulturesAsync(bool installed);
/// <summary>
/// Returns a collection of neutral cultures
/// </summary>
/// <returns></returns>
Task<IEnumerable<Culture>> GetNeutralCulturesAsync();
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -26,6 +32,14 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("Localization");
public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed) => await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed)
{
return await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
}
public async Task<IEnumerable<Culture>> GetNeutralCulturesAsync()
{
return await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}/neutral");
}
}
}

View File

@@ -71,6 +71,15 @@ namespace Oqtane.Services
/// <param name="pageId"></param>
/// <returns></returns>
Task DeletePageAsync(int pageId);
/// <summary>
/// Copies the modules from one page to another
/// </summary>
/// <param name="fromPageId"></param>
/// <param name="toPageId"></param>
/// <param name="usePagePermissions"></param>
/// <returns></returns>
Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -129,5 +138,10 @@ namespace Oqtane.Services
{
await DeleteAsync($"{Apiurl}/{pageId}");
}
public async Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions)
{
await PostAsync($"{Apiurl}/{fromPageId}/{toPageId}/{usePagePermissions}");
}
}
}

View File

@@ -0,0 +1,104 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Linq;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Role"/>s on a <see cref="Site"/>
/// </summary>
public interface ISiteGroupMemberService
{
/// <summary>
/// Get all <see cref="SiteGroupMember"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroupMember>> GetSiteGroupMembersAsync(int siteId, int siteGroupId);
/// <summary>
/// Get one specific <see cref="SiteGroupMember"/>
/// </summary>
/// <param name="siteGroupMemberId">ID-reference of a <see cref="SiteGroupMember"/></param>
/// <returns></returns>
Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteGroupMemberId);
/// <summary>
/// Get one specific <see cref="SiteGroupMember"/>
/// </summary>
/// <param name="siteId">ID-reference of a <see cref="Site"/></param>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteId, int siteGroupId);
/// <summary>
/// Add / save a new <see cref="SiteGroupMember"/> to the database.
/// </summary>
/// <param name="siteGroupMember"></param>
/// <returns></returns>
Task<SiteGroupMember> AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember);
/// <summary>
/// Update a <see cref="SiteGroupMember"/> in the database.
/// </summary>
/// <param name="siteGroupMember"></param>
/// <returns></returns>
Task<SiteGroupMember> UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember);
/// <summary>
/// Delete a <see cref="SiteGroupMember"/> in the database.
/// </summary>
/// <param name="siteGroupMemberId">ID-reference of a <see cref="SiteGroupMember"/></param>
/// <returns></returns>
Task DeleteSiteGroupMemberAsync(int siteGroupMemberId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteGroupMemberService : ServiceBase, ISiteGroupMemberService
{
public SiteGroupMemberService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteGroupMember");
public async Task<List<SiteGroupMember>> GetSiteGroupMembersAsync(int siteId, int siteGroupId)
{
return await GetJsonAsync<List<SiteGroupMember>>($"{Apiurl}?siteid={siteId}&groupid={siteGroupId}", Enumerable.Empty<SiteGroupMember>().ToList());
}
public async Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteGroupMemberId)
{
return await GetJsonAsync<SiteGroupMember>($"{Apiurl}/{siteGroupMemberId}");
}
public async Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteId, int siteGroupId)
{
var siteGroupMembers = await GetSiteGroupMembersAsync(siteId, siteGroupId);
if (siteGroupMembers != null && siteGroupMembers.Count > 0)
{
return siteGroupMembers[0];
}
else
{
return null;
}
}
public async Task<SiteGroupMember> AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember)
{
return await PostJsonAsync<SiteGroupMember>(Apiurl, siteGroupMember);
}
public async Task<SiteGroupMember> UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember)
{
return await PutJsonAsync<SiteGroupMember>($"{Apiurl}/{siteGroupMember.SiteGroupId}", siteGroupMember);
}
public async Task DeleteSiteGroupMemberAsync(int siteGroupMemberId)
{
await DeleteAsync($"{Apiurl}/{siteGroupMemberId}");
}
}
}

View File

@@ -0,0 +1,94 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Linq;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Role"/>s on a <see cref="Site"/>
/// </summary>
public interface ISiteGroupService
{
/// <summary>
/// Get all <see cref="SiteGroup"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroup>> GetSiteGroupsAsync();
/// <summary>
/// Get all <see cref="SiteGroup"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroup>> GetSiteGroupsAsync(int primarySiteId);
/// <summary>
/// Get one specific <see cref="SiteGroup"/>
/// </summary>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task<SiteGroup> GetSiteGroupAsync(int siteGroupId);
/// <summary>
/// Add / save a new <see cref="SiteGroup"/> to the database.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
Task<SiteGroup> AddSiteGroupAsync(SiteGroup siteGroup);
/// <summary>
/// Update a <see cref="SiteGroup"/> in the database.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
Task<SiteGroup> UpdateSiteGroupAsync(SiteGroup siteGroup);
/// <summary>
/// Delete a <see cref="SiteGroup"/> in the database.
/// </summary>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task DeleteSiteGroupAsync(int siteGroupId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteGroupService : ServiceBase, ISiteGroupService
{
public SiteGroupService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteGroup");
public async Task<List<SiteGroup>> GetSiteGroupsAsync()
{
return await GetSiteGroupsAsync(-1);
}
public async Task<List<SiteGroup>> GetSiteGroupsAsync(int primarySiteId)
{
return await GetJsonAsync<List<SiteGroup>>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty<SiteGroup>().ToList());
}
public async Task<SiteGroup> GetSiteGroupAsync(int siteGroupId)
{
return await GetJsonAsync<SiteGroup>($"{Apiurl}/{siteGroupId}");
}
public async Task<SiteGroup> AddSiteGroupAsync(SiteGroup siteGroup)
{
return await PostJsonAsync<SiteGroup>(Apiurl, siteGroup);
}
public async Task<SiteGroup> UpdateSiteGroupAsync(SiteGroup siteGroup)
{
return await PutJsonAsync<SiteGroup>($"{Apiurl}/{siteGroup.SiteGroupId}", siteGroup);
}
public async Task DeleteSiteGroupAsync(int siteGroupId)
{
await DeleteAsync($"{Apiurl}/{siteGroupId}");
}
}
}

View File

@@ -0,0 +1,46 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage tasks (<see cref="SiteTask"/>)
/// </summary>
public interface ISiteTaskService
{
/// <summary>
/// Return a specific task
/// </summary>
/// <param name="siteTaskId"></param>
/// <returns></returns>
Task<SiteTask> GetSiteTaskAsync(int siteTaskId);
/// <summary>
/// Adds a new task
/// </summary>
/// <param name="siteTask"></param>
/// <returns></returns>
Task<SiteTask> AddSiteTaskAsync(SiteTask siteTask);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteTaskService : ServiceBase, ISiteTaskService
{
public SiteTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteTask");
public async Task<SiteTask> GetSiteTaskAsync(int siteTaskId)
{
return await GetJsonAsync<SiteTask>($"{Apiurl}/{siteTaskId}");
}
public async Task<SiteTask> AddSiteTaskAsync(SiteTask siteTask)
{
return await PostJsonAsync<SiteTask>(Apiurl, siteTask);
}
}
}

View File

@@ -161,15 +161,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<string> GetPasswordRequirementsAsync(int siteId);
/// <summary>
/// Bulk import of users
/// </summary>
/// <param name="siteId">ID of a <see cref="Site"/></param>
/// <param name="fileId">ID of a <see cref="File"/></param>
/// <param name="notify">Indicates if new users should be notified by email</param>
/// <returns></returns>
Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify);
/// <summary>
/// Get passkeys for a user
/// </summary>
@@ -351,11 +342,6 @@ namespace Oqtane.Services
return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement);
}
public async Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify)
{
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
public async Task<List<UserPasskey>> GetPasskeysAsync(int userId)
{
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}");

View File

@@ -97,6 +97,7 @@
Alias = PageState.Alias,
Site = new Site
{
SiteId = PageState.Site.SiteId,
DefaultContainerType = PageState.Site.DefaultContainerType,
Settings = PageState.Site.Settings,
Themes = PageState.Site.Themes

View File

@@ -11,6 +11,7 @@
@inject ILogService logger
@inject ISettingService SettingService
@inject IJSRuntime jsRuntime
@inject ISiteGroupService SiteGroupService
@inject IServiceProvider ServiceProvider
@inject ILogService LoggingService
@inject IStringLocalizer<ControlPanelInteractive> Localizer
@@ -34,6 +35,11 @@
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button>
</div>
</div>
@if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId))
{
<hr class="app-rule" />
<button type="button" class="btn btn-secondary col-12 mt-1" @onclick="SynchronizeSite">@Localizer["Synchronize"]</button>
}
<hr class="app-rule" />
}
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -53,18 +59,29 @@
<button type="button" class="btn btn-danger col ms-1" @onclick="ConfirmDelete">@SharedLocalizer["Delete"]</button>
</div>
</div>
<div class="row d-flex">
<div class="col">
@if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button>
}
else
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("publish"))>@Localizer["Page.Publish"]</button>
}
@if (PageState.Page.UserId == null)
{
<div class="row d-flex mb-2">
<div class="col">
<button type="button" class="btn btn-secondary col-12" data-bs-dismiss="offcanvas" @onclick=@(async () => Navigate("Copy"))>@Localizer["Copy"]</button>
</div>
</div>
</div>
}
@if (!PageState.Page.Path.StartsWith("admin/"))
{
<div class="row d-flex">
<div class="col">
@if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button>
}
else
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("publish"))>@Localizer["Page.Publish"]</button>
}
</div>
</div>
}
<hr class="app-rule" />
@if (_deleteConfirmation)
@@ -149,7 +166,7 @@
<option value="@p.PageId">@p.Name</option>
}
</select>
<select class="form-select mt-1" @bind="@_moduleId">
<select class="form-select mt-1" value="@_moduleId" @onchange="(e => PageModuleChanged(e))">
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
@foreach (Module module in _modules)
{
@@ -257,6 +274,7 @@
private List<Page> _pages = new List<Page>();
private List<Module> _modules = new List<Module>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteGroup> _siteGroups = new List<SiteGroup>();
private string _category = "Common";
private string _pane = "";
@@ -285,15 +303,16 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = PageState.Site.DefaultContainerType;
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId);
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList();
_siteGroups = await SiteGroupService.GetSiteGroupsAsync(PageState.Site.SiteId);
}
}
private void CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_moduleDefinitionName = "-";
_message = "";
}
@@ -339,6 +358,20 @@
StateHasChanged();
}
private async Task PageModuleChanged(ChangeEventArgs e)
{
_moduleId = (string)e.Value;
if (_moduleId != "-")
{
_title = _modules.First(item => item.ModuleId == int.Parse(_moduleId)).Title;
}
else
{
_title = "";
}
StateHasChanged();
}
private async Task AddModule()
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -490,6 +523,11 @@
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break;
case "Copy":
// get page management moduleid
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}&copy=true&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break;
}
}
@@ -631,4 +669,14 @@
{
_message = "";
}
private async Task SynchronizeSite()
{
foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId))
{
group.Synchronize = true;
await SiteGroupService.UpdateSiteGroupAsync(group);
}
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true);
}
}

View File

@@ -83,7 +83,10 @@
get => "";
set
{
_showBanner = bool.Parse(value);
if (!bool.TryParse(value, out _showBanner))
{
_showBanner = false;
}
_togglePostback = true;
}
}

View File

@@ -6,22 +6,29 @@
@inject ILocalizationCookieService LocalizationCookieService
@inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1)
@if (PageState.Site.Languages.Count() > 1)
{
<div class="app-languages btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn @ButtonClass dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-globe"></span>
</button>
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@foreach (var culture in _supportedCultures)
@foreach (var language in PageState.Site.Languages)
{
@if (PageState.RenderMode == RenderModes.Interactive)
@if (_contentLocalization)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(culture.Name))" @onclick:preventDefault="true">@culture.DisplayName</a>
<a class="dropdown-item @(PageState.Site.CultureCode == language.Code ? "active" : String.Empty)" href="@(PageState.Alias.Protocol + language.AliasName)" data-enhance-nav="false">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)" data-enhance-nav="false">@culture.DisplayName</a>
@if (PageState.RenderMode == RenderModes.Interactive)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(language.Code))" @onclick:preventDefault="true">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + language.Code)" data-enhance-nav="false">@language.Name</a>
}
}
}
</div>
@@ -29,7 +36,7 @@
}
@code{
private IEnumerable<Culture> _supportedCultures;
private bool _contentLocalization;
private string MenuAlignment = string.Empty;
[Parameter]
@@ -41,14 +48,15 @@
{
MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty;
_supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name });
// determine if site is using content localization
_contentLocalization = PageState.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName));
if (PageState.QueryString.ContainsKey("culture"))
{
var culture = PageState.QueryString["culture"];
if (_supportedCultures.Any(item => item.Name == culture))
if (PageState.Site.Languages.Any(item => item.Code == culture))
{
await LocalizationCookieService.SetLocalizationCookieAsync(culture);
await LocalizationCookieService.SetLocalizationCookieAsync(PageState.Site.CultureCode, culture);
}
NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""));
}
@@ -58,7 +66,7 @@
{
if (culture != CultureInfo.CurrentUICulture.Name)
{
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(PageState.Site.CultureCode, culture));
var interop = new Interop(JSRuntime);
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax");
NavigationManager.NavigateTo(NavigationManager.Uri, true);

View File

@@ -0,0 +1,44 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@inherits ThemeControlBase
@inject IStringLocalizer<Login> Localizer
@inject ISettingService SettingService
@inject IStringLocalizer<SharedResources> SharedLocalizer
<a href="@_registerurl" class="@CssClass">@SharedLocalizer["Register"]</a>
@code
{
private string _returnurl;
private string _registerurl;
[Parameter]
public string CssClass { get; set; } = "btn btn-secondary";
protected override void OnParametersSet()
{
if (!PageState.QueryString.ContainsKey("returnurl"))
{
// remember current url
_returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
}
else
{
// use existing value
_returnurl = PageState.QueryString["returnurl"];
}
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
{
_registerurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "");
_registerurl += (!_registerurl.Contains("?") ? "?" : "&") + "returnurl=" + (_registerurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
_registerurl = NavigateUrl("register", "returnurl=" + _returnurl);
}
Console.WriteLine($"Register URL: {_registerurl}");
}
}

View File

@@ -16,7 +16,7 @@
{
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top")
{
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
<ModuleMessage @key="_messageVersionTop" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
}
@DynamicComponent
@if (_progressIndicator)
@@ -25,7 +25,7 @@
}
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom")
{
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
<ModuleMessage @key="_messageVersionBottom" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
}
}
}
@@ -52,6 +52,8 @@
private MessageStyle _messageStyle;
private bool _progressIndicator = false;
private string _error;
private string _messageVersionTop = Guid.NewGuid().ToString();
private string _messageVersionBottom = Guid.NewGuid().ToString();
[Parameter]
public SiteState SiteState { get; set; }
@@ -143,6 +145,18 @@
_messageStyle = style;
_progressIndicator = false;
if (style == MessageStyle.Toast && !string.IsNullOrEmpty(_messageContent))
{
if (_messagePosition == "top")
{
_messageVersionTop = Guid.NewGuid().ToString();
}
else if (_messagePosition == "bottom")
{
_messageVersionBottom = Guid.NewGuid().ToString();
}
}
StateHasChanged();
}
}

View File

@@ -233,15 +233,8 @@
if (page == null && route.PagePath == "") // naked path refers to site home page
{
if (site.HomePageId != null)
{
page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
}
if (page == null)
{
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
if (page == null)
{
@@ -632,11 +625,11 @@
{
if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site)
{
if (resource.Url.StartsWith("~"))
if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~"))
{
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
}
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{
resource.Url = alias.BaseUrl + resource.Url;
}

View File

@@ -227,38 +227,4 @@
}
return stylesheets;
}
private string ManageScripts(List<Resource> resources, Alias alias)
{
var scripts = "";
if (resources != null)
{
foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Script && item.Location == ResourceLocation.Head))
{
var script = CreateScript(resource, alias);
if (!scripts.Contains(script, StringComparison.OrdinalIgnoreCase))
{
scripts += script + Environment.NewLine;
}
}
}
return scripts;
}
private string CreateScript(Resource resource, Alias alias)
{
if (!string.IsNullOrEmpty(resource.Url))
{
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
return "<script src=\"" + url + "\"" +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
"></script>";
}
else
{
// inline script
return "<script>" + resource.Content + "</script>";
}
}
}

View File

@@ -11,6 +11,7 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Oqtane.Client

View File

@@ -18,7 +18,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>10.0.3</ApplicationDisplayVersion>
<ApplicationDisplayVersion>10.1.2</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@@ -54,11 +54,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.5" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>10.0.3</version>
<version>10.1.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,18 +12,18 @@
<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/v10.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net10.0">
<dependency id="Oqtane.Shared" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="8.4.0" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="10.0.6" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Framework</id>
<version>10.0.3</version>
<version>10.1.2</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/v10.0.3/Oqtane.Framework.10.0.3.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.1.2/Oqtane.Framework.10.1.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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>10.0.3</version>
<version>10.1.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,29 +12,29 @@
<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/v10.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net10.0">
<dependency id="Oqtane.Client" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Oqtane.Client" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="SixLabors.ImageSharp" version="3.1.12" exclude="Build,Analyzers" />
<dependency id="HtmlAgilityPack" version="1.12.4" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" />
<dependency id="MySql.Data" version="9.5.0" exclude="Build,Analyzers" />
<dependency id="Pomelo.EntityFrameworkCore.MySql" version="9.0.0" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="10.0.0-rc.2" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="10.1.7" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.15.1" exclude="Build,Analyzers" />
<dependency id="MySql.Data" version="9.6.0" exclude="Build,Analyzers" />
<dependency id="MySql.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.5" exclude="Build,Analyzers" />
</group>
</dependencies>
<frameworkReferences>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>10.0.3</version>
<version>10.1.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,15 +12,15 @@
<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/v10.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net10.0">
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.2.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.3.1" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>10.0.3</version>
<version>10.1.2</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/v10.0.2</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.3.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.2.Install.zip" -Force

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.3.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.2.Upgrade.zip" -Force

View File

@@ -79,6 +79,7 @@
@((MarkupString)_scripts)
@((MarkupString)_bodyResources)
@((MarkupString)_bodyContent)
@if (_renderMode == RenderModes.Static)
{
<page-script src="./js/reload.js?v=@_fingerprint"></page-script>
@@ -107,6 +108,7 @@
private string _language = "en";
private string _headResources = "";
private string _bodyResources = "";
private string _bodyContent = "";
private string _styleSheets = "";
private string _scripts = "";
private string _message = "";
@@ -157,15 +159,8 @@
var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
if (page == null && route.PagePath == "") // naked path refers to site home page
{
if (site.HomePageId != null)
{
page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
}
if (page == null)
{
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
if (page == null)
{
@@ -193,43 +188,20 @@
await GetJwtToken(alias);
}
// includes resources
// include resources
var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action);
ManageStyleSheets(resources);
ManageScripts(resources, alias);
AddBodyContent(site.BodyContent);
AddBodyContent(page.BodyContent);
_language = ManageLocalization(alias, site);
// generate scripts
// PWA script
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
{
_scripts += CreatePWAScript(alias, site, route);
}
// set culture if not specified
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
if (cultureCookie == null)
{
// get default language for site
if (site.Languages.Any())
{
// use default language if specified otherwise use first language in collection
cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
}
else
{
// fallback language
cultureCookie = LocalizationManager.GetDefaultCulture();
}
// convert language code to culture cookie format (ie. "c=en|uic=en")
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie));
SetLocalizationCookie(cultureCookie);
}
// set language for page
if (!string.IsNullOrEmpty(cultureCookie))
{
_language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name;
}
// create initial PageState
_pageState = new PageState
{
@@ -294,6 +266,28 @@
private void HandlePageNotFound(Site site, Page page, Route route)
{
// filter
string useragent = (Context.Request.Headers[HeaderNames.UserAgent] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.UserAgent] : "(none)";
var settings = Context.GetSiteSettings();
var filter = settings.GetValue("VisitorFilter", Constants.DefaultVisitorFilter);
foreach (string term in filter.ToLower().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
{
if (_remoteIPAddress.ToLower().Contains(term) || useragent.ToLower().Contains(term))
{
// handle not found request in static mode
if (_renderMode == RenderModes.Static)
{
NavigationManager.NotFound();
}
else
{
// redirect to 404 page
NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
}
return;
}
}
// referrer will only be set if the link originated externally
string referrer = (Context.Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.Referer] : "";
@@ -550,8 +544,6 @@
}
else
{
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
var dataAttributes = "";
if (!resource.DataAttributes.ContainsKey("data-reload"))
{
@@ -573,32 +565,27 @@
}
}
return "<script src=\"" + url + "\"" +
((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>";
if (!string.IsNullOrEmpty(resource.Url))
{
var url = (string.IsNullOrEmpty(resource.Url) || resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
return "<script src=\"" + url + "\"" +
((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>";
}
else
{
return "<script" +
((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
">" + resource.Content + "</script>";
}
}
}
private void SetLocalizationCookie(string cookieValue)
{
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
};
Context.Response.Cookies.Append(
Shared.CookieRequestCultureProvider.DefaultCookieName,
cookieValue,
cookieOptions
);
}
private async Task<List<Resource>> GetPageResources(Alias alias, Site site, Page page, List<Module> modules, int moduleid, string action)
{
var resources = new List<Resource>();
@@ -751,11 +738,11 @@
{
if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site)
{
if (resource.Url.StartsWith("~"))
if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~"))
{
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
}
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{
resource.Url = alias.BaseUrl + resource.Url;
}
@@ -803,4 +790,103 @@
}
}
}
private void AddBodyContent(string content)
{
if (!string.IsNullOrEmpty(content))
{
var elements = content.Split('<', StringSplitOptions.RemoveEmptyEntries);
foreach (var element in elements)
{
if (_renderMode == RenderModes.Static || (!element.ToLower().StartsWith("script") && !element.ToLower().StartsWith("/script")))
{
if (!_bodyContent.Contains("<" + element) || element.StartsWith("/"))
{
_bodyContent += "<" + element;
}
}
}
_bodyContent += "\n";
}
}
private string ManageLocalization(Alias alias, Site site)
{
// get site culture (content localization)
var culture = site.CultureCode;
if (string.IsNullOrEmpty(culture))
{
// use fallback language
culture = LocalizationManager.GetDefaultCulture();
}
// determine if site is using content localization
var contentLocalization = site.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName));
// get user culture (ui localization)
var uiCulture = site.User?.CultureCode;
// if user has not specified a UI culture
if (string.IsNullOrEmpty(uiCulture))
{
// if not using content localization
if (!contentLocalization)
{
// use default language if specified otherwise use first language in collection
uiCulture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
}
else
{
// use site culture
uiCulture = culture;
}
}
else
{
// if not using content localization and UI culture is not supported by site
if (!contentLocalization && !site.Languages.Any(item => item.Code == uiCulture))
{
// use site culture
uiCulture = culture;
}
}
// get culture cookie
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
if (cultureCookie == null)
{
// convert to culture cookie format (ie. "c=en|uic=en") and save cookie
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture));
SetLocalizationCookie(cultureCookie);
}
else
{
// validate the culture cookie
var requestCulture = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie);
if (requestCulture?.Culture.Name != culture || requestCulture?.UICulture.Name != uiCulture)
{
// convert to culture cookie format (ie. "c=en|uic=en") and save cookie
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture));
SetLocalizationCookie(cultureCookie);
}
}
return culture; // html element language attribute for page
}
private void SetLocalizationCookie(string cookieValue)
{
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
};
Context.Response.Cookies.Append(
Shared.CookieRequestCultureProvider.DefaultCookieName,
cookieValue,
cookieOptions
);
}
}

View File

@@ -43,7 +43,7 @@ namespace Oqtane.Controllers
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
var hierarchy = GetFoldersHierarchy(_folders.GetFolders(SiteId).ToList());
var hierarchy = _folders.GetFolders(SiteId).ToList();
foreach (Folder folder in hierarchy)
{
// note that Browse permission is used for this method
@@ -281,47 +281,5 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
private static List<Folder> GetFoldersHierarchy(List<Folder> folders)
{
List<Folder> hierarchy = new List<Folder>();
Action<List<Folder>, Folder> getPath = null;
getPath = (folderList, folder) =>
{
IEnumerable<Folder> children;
int level;
if (folder == null)
{
level = -1;
children = folders.Where(item => item.ParentId == null);
}
else
{
level = folder.Level;
children = folders.Where(item => item.ParentId == folder.FolderId);
}
foreach (Folder child in children)
{
child.Level = level + 1;
child.HasChildren = folders.Any(item => item.ParentId == child.FolderId);
hierarchy.Add(child);
getPath(folderList, child);
}
};
folders = folders.OrderBy(item => item.Name).ToList();
getPath(folders, null);
// add any non-hierarchical items to the end of the list
foreach (Folder folder in folders)
{
if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null)
{
hierarchy.Add(folder);
}
}
return hierarchy;
}
}
}

View File

@@ -19,26 +19,54 @@ namespace Oqtane.Controllers
_localizationManager = localizationManager;
}
// GET: api/localization
// GET: api/localization?installed=true/false
[HttpGet()]
public IEnumerable<Culture> Get(bool installed)
{
string[] culturecodes;
string[] cultureCodes;
if (installed)
{
culturecodes = _localizationManager.GetInstalledCultures();
cultureCodes = _localizationManager.GetInstalledCultures();
}
else
{
culturecodes = _localizationManager.GetSupportedCultures();
cultureCodes = _localizationManager.GetSupportedCultures();
}
var cultures = culturecodes.Select(c => new Culture
var cultures = cultureCodes.Select(c => new Culture
{
Name = CultureInfo.GetCultureInfo(c).Name,
DisplayName = CultureInfo.GetCultureInfo(c).DisplayName,
IsDefault = _localizationManager.GetDefaultCulture()
.Equals(CultureInfo.GetCultureInfo(c).Name, StringComparison.OrdinalIgnoreCase)
});
}).ToList();
if (cultures.Count == 0)
{
cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = true });
}
return cultures.OrderBy(item => item.DisplayName);
}
// GET: api/localization/neutral
[HttpGet("neutral")]
public IEnumerable<Culture> Get()
{
var cultureCodes = _localizationManager.GetNeutralCultures();
var cultures = cultureCodes.Select(c => new Culture
{
Name = CultureInfo.GetCultureInfo(c).Name,
DisplayName = CultureInfo.GetCultureInfo(c).DisplayName,
IsDefault = false
}).ToList();
if (cultures.Count == 0)
{
cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = false });
}
return cultures.OrderBy(item => item.DisplayName);
}
}

View File

@@ -264,7 +264,7 @@ namespace Oqtane.Controllers
_userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename))
{
// get content
var content = _modules.ExportModule(moduleid);
var content = _modules.ExportModule(module, "Export Module");
// get folder
var folder = _folders.GetFolder(folderid, false);
@@ -317,7 +317,7 @@ namespace Oqtane.Controllers
var module = _modules.GetModule(moduleid);
if (ModelState.IsValid && module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit))
{
success = _modules.ImportModule(moduleid, content);
success = _modules.ImportModule(module, content, "Import Module");
if (success)
{
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Content Imported {ModuleId}", moduleid);

View File

@@ -164,6 +164,7 @@ namespace Oqtane.Controllers
{
_moduleDefinitions.UpdateModuleDefinition(moduleDefinition);
_syncManager.AddSyncEvent(_alias, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Definition Updated {ModuleDefinition}", moduleDefinition);
}
else

View File

@@ -1,16 +1,14 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using Oqtane.Shared;
using System.Linq;
using Oqtane.Security;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using System.Xml.Linq;
using Microsoft.AspNetCore.Diagnostics;
using Oqtane.Security;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
@@ -240,10 +238,11 @@ namespace Oqtane.Controllers
};
module = _modules.AddModule(module);
string content = _modules.ExportModule(pm.ModuleId);
// deep copy module content (includes settings)
string content = _modules.ExportModule(pm.Module, "Copy Page");
if (content != "")
{
_modules.ImportModule(module.ModuleId, content);
_modules.ImportModule(module, content, "Copy Page");
}
PageModule pagemodule = new PageModule();
@@ -498,6 +497,107 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
// POST api/<controller>/5/6
[HttpPost("{fromPageId}/{toPageId}/{usePagePermissions}")]
[Authorize(Roles = RoleNames.Registered)]
public void Post(int fromPageId, int toPageId, bool usePagePermissions)
{
var fromPage = _pages.GetPage(fromPageId);
if (fromPage != null && fromPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, fromPage.PermissionList))
{
var toPage = _pages.GetPage(toPageId);
if (toPage != null && toPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, toPage.PermissionList))
{
// copy page settings
var settings = _settings.GetSettings(EntityNames.Page, fromPage.PageId).ToList();
foreach (var setting in settings)
{
_settings.AddSetting(new Setting
{
EntityName = setting.EntityName,
EntityId = toPage.PageId,
SettingName = setting.SettingName,
SettingValue = setting.SettingValue,
IsPrivate = setting.IsPrivate
});
}
// copy modules
List<PageModule> pageModules = _pageModules.GetPageModules(fromPage.SiteId).ToList();
foreach (PageModule pm in pageModules.Where(item => item.PageId == fromPage.PageId && !item.Module.AllPages && !item.IsDeleted))
{
Module module;
// determine if module is a shared instance (ie. exists on other pages)
if (!pageModules.Any(item => item.ModuleId == pm.ModuleId && item.PageId != fromPage.PageId))
{
// create new module
module = new Module();
module.SiteId = fromPage.SiteId;
module.PageId = toPageId;
module.ModuleDefinitionName = pm.Module.ModuleDefinitionName;
module.AllPages = false;
if (usePagePermissions)
{
module.PermissionList = toPage.PermissionList;
}
else
{
module.PermissionList = pm.Module.PermissionList;
}
module.PermissionList = module.PermissionList.Select(item => new Permission
{
SiteId = item.SiteId,
EntityName = EntityNames.Module,
EntityId = -1,
PermissionName = item.PermissionName,
RoleName = item.RoleName,
UserId = item.UserId,
IsAuthorized = item.IsAuthorized,
}).ToList();
module = _modules.AddModule(module);
// deep copy module content (includes settings)
string content = _modules.ExportModule(pm.Module, "Copy Page");
if (content != "")
{
_modules.ImportModule(module, content, "Copy Page");
}
}
else
{
// use existing module
module = pm.Module;
}
PageModule pageModule = new PageModule();
pageModule.PageId = toPageId;
pageModule.ModuleId = module.ModuleId;
pageModule.Title = pm.Title;
pageModule.Pane = pm.Pane;
pageModule.Order = pm.Order;
pageModule.ContainerType = pm.ContainerType;
pageModule.EffectiveDate = pm.EffectiveDate;
pageModule.ExpiryDate = pm.ExpiryDate;
pageModule.Header = pm.Header;
pageModule.Footer = pm.Footer;
_pageModules.AddPageModule(pageModule);
}
_syncManager.AddSyncEvent(_alias, EntityNames.Site, fromPage.SiteId, SyncEventActions.Refresh);
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -0,0 +1,135 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Policy;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SiteGroupController : Controller
{
private readonly ISiteGroupRepository _siteGroupRepository;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteGroupController(ISiteGroupRepository siteGroupRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{
_siteGroupRepository = siteGroupRepository;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>?siteid=x
[HttpGet]
[Authorize(Roles = RoleNames.Admin)]
public IEnumerable<SiteGroup> Get(string siteid)
{
if (User.IsInRole(RoleNames.Host) || (int.TryParse(siteid, out int SiteId) && SiteId == _alias.SiteId))
{
var siteGroups = _siteGroupRepository.GetSiteGroups();
if (!User.IsInRole(RoleNames.Host))
{
siteGroups = siteGroups.Where(item => item.PrimarySiteId == _alias.SiteId);
}
return siteGroups.ToList();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Host)]
public SiteGroup Get(int id)
{
var group = _siteGroupRepository.GetSiteGroup(id);
if (group != null)
{
return group;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Host)]
public SiteGroup Post([FromBody] SiteGroup siteGroup)
{
if (ModelState.IsValid)
{
siteGroup = _siteGroupRepository.AddSiteGroup(siteGroup);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {Group}", siteGroup);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Post Attempt {Group}", siteGroup);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroup = null;
}
return siteGroup;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Admin)]
public SiteGroup Put(int id, [FromBody] SiteGroup siteGroup)
{
if (ModelState.IsValid && siteGroup.SiteGroupId == id)
{
if (!User.IsInRole(RoleNames.Host) && siteGroup.Synchronize)
{
// admins can only update the synchronize field
siteGroup = _siteGroupRepository.GetSiteGroup(siteGroup.SiteGroupId, false);
siteGroup.Synchronize = true;
}
siteGroup = _siteGroupRepository.UpdateSiteGroup(siteGroup);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {Group}", siteGroup);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Put Attempt {Group}", siteGroup);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroup = null;
}
return siteGroup;
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Host)]
public void Delete(int id)
{
var siteGroup = _siteGroupRepository.GetSiteGroup(id);
if (siteGroup != null)
{
_siteGroupRepository.DeleteSiteGroup(id);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {siteGroupId}", id);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {siteGroupId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SiteGroupMemberController : Controller
{
private readonly ISiteGroupMemberRepository _siteGroupMemberRepository;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteGroupMemberController(ISiteGroupMemberRepository siteGroupMemberRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{
_siteGroupMemberRepository = siteGroupMemberRepository;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>?siteid=x&groupid=y
[HttpGet]
[Authorize(Roles = RoleNames.Host)]
public IEnumerable<SiteGroupMember> Get(string siteid, string groupid)
{
if (int.TryParse(siteid, out int SiteId) && int.TryParse(groupid, out int SiteGroupId))
{
return _siteGroupMemberRepository.GetSiteGroupMembers(SiteId, SiteGroupId).ToList();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Get Attempt for SiteId {SiteId} And SiteGroupId {SiteGroupId}", siteid, groupid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Host)]
public SiteGroupMember Get(int id)
{
var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id);
if (siteGroupMember != null)
{
return siteGroupMember;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Host)]
public SiteGroupMember Post([FromBody] SiteGroupMember siteGroupMember)
{
if (ModelState.IsValid)
{
siteGroupMember = _siteGroupMemberRepository.AddSiteGroupMember(siteGroupMember);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Member Added {SiteGroupMember}", siteGroupMember);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Post Attempt {SiteGroupMember}", siteGroupMember);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroupMember = null;
}
return siteGroupMember;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Host)]
public SiteGroupMember Put(int id, [FromBody] SiteGroupMember siteGroupMember)
{
if (ModelState.IsValid && siteGroupMember.SiteGroupId == id && _siteGroupMemberRepository.GetSiteGroupMember(siteGroupMember.SiteGroupId, false) != null)
{
siteGroupMember = _siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Member Updated {SiteGroupMember}", siteGroupMember);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Put Attempt {SiteGroupMember}", siteGroupMember);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroupMember = null;
}
return siteGroupMember;
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Host)]
public void Delete(int id)
{
var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id);
if (siteGroupMember != null)
{
_siteGroupMemberRepository.DeleteSiteGroupMember(id);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupMemberId, SyncEventActions.Delete);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Member Deleted {SiteGroupMemberId}", id);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Delete Attempt {SiteGroupMemberId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SiteTaskController : Controller
{
private readonly ISiteTaskRepository _siteTasks;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteTaskController(ISiteTaskRepository siteTasks, ILogManager logger, ITenantManager tenantManager)
{
_siteTasks = siteTasks;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Admin)]
public SiteTask Get(int id)
{
var siteTask = _siteTasks.GetSiteTask(id);
if (siteTask.SiteId == _alias.SiteId)
{
return siteTask;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
public SiteTask Post([FromBody] SiteTask siteTask)
{
if (ModelState.IsValid && siteTask.SiteId == _alias.SiteId)
{
siteTask.IsCompleted = false;
siteTask = _siteTasks.AddSiteTask(siteTask);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Task Added {SiteTask}", siteTask);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Task Post Attempt {SiteTask}", siteTask);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteTask = null;
}
return siteTask;
}
}
}

View File

@@ -128,7 +128,7 @@ namespace Oqtane.Controllers
}
break;
default:
_configManager.AddOrUpdateSetting(key, value, false);
_configManager.AddOrUpdateSetting(key, value, true);
break;
}
}

View File

@@ -117,6 +117,7 @@ namespace Oqtane.Controllers
{
_themes.UpdateTheme(theme);
_syncManager.AddSyncEvent(_alias, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme);
}
else

View File

@@ -418,42 +418,6 @@ namespace Oqtane.Controllers
return requirements;
}
// POST api/<controller>/import?siteid=x&fileid=y&notify=z
[HttpPost("import")]
[Authorize(Roles = RoleNames.Admin)]
public async Task<Dictionary<string, string>> Import(string siteid, string fileid, string notify)
{
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId) && bool.TryParse(notify, out bool Notify))
{
var file = _files.GetFile(FileId);
if (file != null)
{
if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
return await _userManager.ImportUsers(SiteId, _files.GetFilePath(file), Notify);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Import File Does Not Exist {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET: api/<controller>/passkey?id=x
[HttpGet("passkey")]
[Authorize]

View File

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using MySql.Data.MySqlClient;
using MySql.EntityFrameworkCore.Metadata;
using Oqtane.Databases;
namespace Oqtane.Database.MySQL
@@ -25,7 +26,7 @@ namespace Oqtane.Database.MySQL
public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name)
{
return table.Column<int>(name: name, nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
return table.Column<int>(name: name, nullable: false).Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn);
}
public override string ConcatenateSql(params string[] values)
@@ -96,7 +97,7 @@ namespace Oqtane.Database.MySQL
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{
return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
return optionsBuilder.UseMySQL(connectionString);
}
private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query)

View File

@@ -67,8 +67,8 @@ namespace Oqtane.Extensions
app.UseOutputCache();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.UseNotFoundResponse();
app.UseAntiforgery();
// execute any IServerStartup logic
app.ConfigureOqtaneAssemblies(environment);
@@ -153,28 +153,33 @@ namespace Oqtane.Extensions
public static IApplicationBuilder UseNotFoundResponse(this IApplicationBuilder app)
{
// set the route to be rendered on NavigationManager.NotFound()
const string notFoundRoute = "/404";
app.UseStatusCodePagesWithReExecute(notFoundRoute, createScopeForStatusCodePages: true);
// middleware to determine if status code pages should be skipped
app.Use(async (context, next) =>
{
var path = context.Request.Path.Value ?? string.Empty;
if (string.IsNullOrEmpty(path) || ShouldSkipStatusCodeReExecution(path))
if (ShouldSkipStatusCodeReExecution(path))
{
var feature = context.Features.Get<IStatusCodePagesFeature>();
feature?.Enabled = false;
var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}
}
await next();
});
// middleware to rewrite the path for 404 status code responses on sites using subfolders
app.Use(async (context, next) =>
{
var feature = context.Features.Get<IStatusCodeReExecuteFeature>();
var handled = false;
if (feature != null
var statusCodeReExecuteFeature = context.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null
&& context.Response.StatusCode == (int)HttpStatusCode.NotFound
&& notFoundRoute.Equals(context.Request.Path.Value, StringComparison.OrdinalIgnoreCase))
&& string.Equals(context.Request.Path.Value, notFoundRoute, StringComparison.OrdinalIgnoreCase))
{
var alias = context.GetAlias();
if (!string.IsNullOrEmpty(alias?.Path))
@@ -183,20 +188,17 @@ namespace Oqtane.Extensions
context.Request.Path = new PathString($"/{alias.Path}{notFoundRoute}");
try
{
handled = true;
await next();
}
finally
{
context.Request.Path = originalPath;
}
return;
}
}
if (!handled)
{
await next();
}
await next();
});
return app;
@@ -204,12 +206,16 @@ namespace Oqtane.Extensions
static bool ShouldSkipStatusCodeReExecution(string path)
{
return Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) || HasStaticFileExtension(path);
// skip requests for framework resources, reserved routes, and static files
return path.StartsWith("/_framework/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/_content/", StringComparison.OrdinalIgnoreCase) ||
Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) ||
HasStaticFileExtension(path);
}
static bool HasStaticFileExtension(string path)
{
return !string.IsNullOrEmpty(Path.GetExtension(path));
return !string.IsNullOrEmpty(Path.GetExtension(path)) && Path.GetExtension(path).Length != 1;
}
}
}

View File

@@ -233,6 +233,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ICookieConsentService, ServerCookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<ISiteGroupService, SiteGroupService>();
services.AddScoped<ISiteGroupMemberService, SiteGroupMemberService>();
services.AddScoped<ISiteTaskService, SiteTaskService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
@@ -282,6 +285,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<IUrlMappingRepository, UrlMappingRepository>();
services.AddTransient<ISearchContentRepository, SearchContentRepository>();
services.AddTransient<IMigrationHistoryRepository, MigrationHistoryRepository>();
services.AddTransient<ISiteGroupRepository, SiteGroupRepository>();
services.AddTransient<ISiteGroupMemberRepository, SiteGroupMemberRepository>();
services.AddTransient<ISiteTaskRepository, SiteTaskRepository>();
// managers
services.AddTransient<IDBContextDependencies, DBContextDependencies>();

View File

@@ -84,16 +84,13 @@ namespace Oqtane.Extensions
options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{
options.Events = new OpenIdConnectEvents
options.Events.OnRedirectToIdentityProvider = context =>
{
OnRedirectToIdentityProvider = context =>
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
}
return Task.FromResult(0);
context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
}
return Task.FromResult(0);
};
}
}
@@ -132,18 +129,15 @@ namespace Oqtane.Extensions
options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{
options.Events = new OAuthEvents
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
OnRedirectToAuthorizationEndpoint = context =>
var url = context.RedirectUri;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
var url = context.RedirectUri;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
}
context.Response.Redirect(url);
return Task.FromResult(0);
url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
}
context.Response.Redirect(url);
return Task.FromResult(0);
};
}
}

View File

@@ -594,7 +594,8 @@ namespace Oqtane.Infrastructure
Prerender = (rendermode == RenderModes.Interactive),
Hybrid = false,
EnhancedNavigation = true,
TenantId = tenant.TenantId
CultureCode = "en",
TenantId = tenant.TenantId // required for site creation
};
site = sites.AddSite(site);

View File

@@ -22,7 +22,6 @@ namespace Oqtane.Infrastructure
{
void InstallPackages();
bool UninstallPackage(string PackageName);
int RegisterAssemblies();
Task UpgradeFramework(bool backup);
void RestartApplication();
}
@@ -61,10 +60,6 @@ namespace Oqtane.Infrastructure
Directory.CreateDirectory(sourceFolder);
}
// read assembly log
var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
// install Nuget packages in secure Packages folder
var packages = Directory.GetFiles(sourceFolder, "*.nupkg");
foreach (string packagename in packages)
@@ -162,27 +157,6 @@ namespace Oqtane.Infrastructure
{
manifest = true;
}
// register assembly
if (Path.GetExtension(filename) == ".dll")
{
// do not register licensing assemblies
if (!Path.GetFileName(filename).StartsWith("Oqtane.Licensing."))
{
// if package version was not installed previously
if (!File.Exists(Path.Combine(sourceFolder, name + ".log")))
{
if (assemblies.ContainsKey(Path.GetFileName(filename)))
{
assemblies[Path.GetFileName(filename)] += 1;
}
else
{
assemblies.Add(Path.GetFileName(filename), 1);
}
}
}
}
}
}
@@ -212,12 +186,6 @@ namespace Oqtane.Infrastructure
File.Delete(packagename);
}
if (packages.Length != 0)
{
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
}
return errors;
}
@@ -270,10 +238,6 @@ namespace Oqtane.Infrastructure
{
if (!string.IsNullOrEmpty(PackageName))
{
// read assembly log
var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
// get manifest with highest version
string packagename = "";
string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log");
@@ -298,23 +262,7 @@ namespace Oqtane.Infrastructure
// do not remove licensing assemblies
if (!Path.GetFileName(filepath).StartsWith("Oqtane.Licensing."))
{
// use assembly log to determine if assembly is used in other packages
if (assemblies.ContainsKey(Path.GetFileName(filepath)))
{
if (assemblies[Path.GetFileName(filepath)] == 1)
{
DeleteFile(filepath);
assemblies.Remove(Path.GetFileName(filepath));
}
else
{
assemblies[Path.GetFileName(filepath)] -= 1;
}
}
else // does not exist in assembly log
{
DeleteFile(filepath);
}
DeleteFile(filepath);
}
}
else // not an assembly
@@ -329,9 +277,6 @@ namespace Oqtane.Infrastructure
File.Delete(asset);
}
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
return true;
}
}
@@ -351,64 +296,6 @@ namespace Oqtane.Infrastructure
}
}
public int RegisterAssemblies()
{
var assemblyLogPath = GetAssemblyLogPath();
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblies = GetAssemblyLog(assemblyLogPath);
// remove assemblies that no longer exist
foreach (var dll in assemblies)
{
if (!File.Exists(Path.Combine(binFolder, dll.Key)))
{
assemblies.Remove(dll.Key);
}
}
// add assemblies which are not registered
foreach (var dll in Directory.GetFiles(binFolder, "*.dll"))
{
if (!assemblies.ContainsKey(Path.GetFileName(dll)))
{
assemblies.Add(Path.GetFileName(dll), 1);
}
}
SetAssemblyLog(assemblyLogPath, assemblies);
return assemblies.Count;
}
private string GetAssemblyLogPath()
{
string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
if (!Directory.Exists(packagesFolder))
{
Directory.CreateDirectory(packagesFolder);
}
return Path.Combine(packagesFolder, "assemblies.log");
}
private static Dictionary<string, int> GetAssemblyLog(string assemblyLogPath)
{
Dictionary<string, int> assemblies = new Dictionary<string, int>();
if (File.Exists(assemblyLogPath))
{
assemblies = JsonSerializer.Deserialize<Dictionary<string, int>>(File.ReadAllText(assemblyLogPath));
}
return assemblies;
}
private static void SetAssemblyLog(string assemblyLogPath, Dictionary<string, int> assemblies)
{
if (File.Exists(assemblyLogPath))
{
File.Delete(assemblyLogPath);
}
File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true }));
}
public async Task UpgradeFramework(bool backup)
{
string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);

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