Compare commits

...

97 Commits

Author SHA1 Message Date
19fb3120d3 Update README.md 2020-05-19 14:18:43 -04:00
4154bd0988 Merge pull request #509 from sbwalker/master
update to version 1.0.0
2020-05-19 14:07:10 -04:00
35b26c7525 update to version 1.0.0 2020-05-19 14:08:03 -04:00
52b2c876a4 performance optimizations to avoid use of reflection ( thanks to @chlupac for this suggestion ) 2020-05-19 13:39:39 -04:00
b7eeda7562 Merge pull request #508 from sbwalker/master
performance optimizations to avoid use of reflection ( thanks to @chlupac for this suggestion )
2020-05-19 13:38:30 -04:00
b59e2533ea update to .NET Core 3.2 2020-05-19 12:48:30 -04:00
a8ec5e1265 Merge pull request #507 from sbwalker/master
update to .NET Core 3.2
2020-05-19 12:47:36 -04:00
61ff097f51 default theme needs to work in local development scenarios 2020-05-19 09:17:38 -04:00
b82f69b8b0 Merge pull request #506 from sbwalker/master
default theme needs to work in local development scenarios
2020-05-19 09:16:37 -04:00
263b045c75 module creator template updates 2020-05-19 09:08:29 -04:00
a160597f6f Merge pull request #505 from sbwalker/master
module creator template updates
2020-05-19 09:07:25 -04:00
f0fe6551dd Merge pull request #24 from oqtane/master
sync
2020-05-19 08:24:12 -04:00
09b17e142e Merge pull request #503 from jimspillane/FixMergeFiles
Fix file upload merge
2020-05-19 07:52:07 -04:00
b8ce634f85 Fix file upload merge
Allow upload of file names that match the token pattern ".part_", but not in the file extension. For example, a file named, a.part_Y.txt, would not be uploaded.
2020-05-18 19:53:49 -04:00
1532eb7586 Optimized downloading of assemblies when using WebAssembly 2020-05-18 18:02:23 -04:00
9b65cd0e07 Merge pull request #502 from sbwalker/master
Optimized downloading of assemblies when using WebAssembly
2020-05-18 18:01:23 -04:00
b4596cb624 Merge pull request #11 from oqtane/master
Sync upstream
2020-05-18 15:30:43 -04:00
5e23448618 use lambas for setting resources 2020-05-18 13:34:53 -04:00
96c2a59551 Merge pull request #501 from sbwalker/master
use lambas for setting resources
2020-05-18 13:33:50 -04:00
6ae019336d changing default theme to Oqtane theme 2020-05-18 10:44:54 -04:00
0c236682b9 Merge pull request #500 from sbwalker/master
changing default theme to Oqtane theme
2020-05-18 10:44:00 -04:00
9b74262c76 Added support for module resource management 2020-05-18 09:47:37 -04:00
a13208e65d Merge pull request #499 from sbwalker/master
Added support for module resource management
2020-05-18 09:46:32 -04:00
f8ab886750 Fixed issue with loading resources 2020-05-16 22:11:58 -04:00
84b011224e Merge pull request #498 from sbwalker/master
Fixed issue with loading resources
2020-05-16 22:11:00 -04:00
c426302242 enhanced module creator to display location where module will be created 2020-05-16 13:40:59 -04:00
814e2100b2 Merge pull request #495 from sbwalker/master
enhanced module creator to display location where module will be created
2020-05-16 13:39:57 -04:00
54d4447d23 Central management of resources ( ie. stylesheets and scripts ) 2020-05-16 12:00:15 -04:00
93942d7cdd Merge pull request #492 from jimspillane/AddMissingSInCsproj
Fix module pluralization
2020-05-16 12:00:08 -04:00
0afd7d9ca4 Merge pull request #493 from sbwalker/master
Central management of resources ( ie. stylesheets and scripts )
2020-05-16 11:59:24 -04:00
3a19ced2d1 Fix module pluralization
Added 's' to the module creator sql script in the csproj to fix compilation error.
2020-05-16 10:36:16 -04:00
d5811a20e9 Merge pull request #10 from oqtane/master
Sync master
2020-05-16 10:25:28 -04:00
1b2600c6c4 Merge pull request #23 from oqtane/master
sync
2020-05-16 08:54:28 -04:00
f9cdc6d70c Merge pull request #490 from jimspillane/AddFileValidation
Add File Name validation
2020-05-16 08:53:39 -04:00
fbdf21320b Merge pull request #491 from chlupac/SettingsService
Setting service bug.
2020-05-16 08:52:13 -04:00
96f5668a3b Setting service bug. 2020-05-16 08:40:30 +02:00
13adebb36c Add File Name validation
Apply file name validation rules to the File Controller and client.
2020-05-15 23:12:24 -04:00
8a1e83ff7f Modified the package installer to use target folders ( based on the Nuget specification ) rather than file extensions 2020-05-15 17:43:45 -04:00
e698ea4d36 Merge pull request #489 from sbwalker/master
Modified the package installer to use target folders  ( based on the Nuget specification ) rather than file extensions
2020-05-15 17:42:49 -04:00
f5ce00ae7d Merge pull request #9 from oqtane/master
Sync upstream master
2020-05-15 12:56:45 -04:00
3cbb6e3e6e enable module creator to add embeddedresources to csproj for internal modules 2020-05-15 12:36:52 -04:00
9394e77fd5 Merge pull request #488 from sbwalker/master
enable module creator to add embeddedresources to csproj for internal modules
2020-05-15 12:35:52 -04:00
ac03afb146 added ability to set default container at the page level, expanded size of role description in upgrade script for 0.9.2 2020-05-15 09:50:48 -04:00
51c27ae0e5 Merge pull request #486 from sbwalker/master
added ability to set default container at the page level, expanded size of role description in upgrade script for 0.9.2
2020-05-15 09:49:45 -04:00
0ea4c4d723 Merge pull request #484 from jimspillane/MoveFolderValidationToShared
Move Path and File validation to Shared Utilities
2020-05-15 09:47:37 -04:00
b3dee737b4 Merge pull request #480 from chlupac/FileManager
FileManager Tune-Up
2020-05-15 09:46:28 -04:00
24ca9f4ded Merge pull request #485 from chlupac/FileController
File Controller bug
2020-05-15 07:28:12 -04:00
9850e249fc File Controller bug 2020-05-15 08:20:00 +02:00
5e04cb18a4 File Manager Tune-up 2020-05-15 08:18:07 +02:00
39641804f1 Move Path and File validation to Shared Utilities
Created extension methods:
IsPathValid(Folder)
IsFileValid(File)
IsPathOrFileValid(string)

Added client side validation check for Folders.
2020-05-14 22:02:57 -04:00
edc356292d Merge pull request #481 from thabaum/patch-6
container class added to pane
2020-05-14 21:34:09 -04:00
78f4af6b70 Merge pull request #482 from thabaum/patch-7
System Info input disabled changed to readonly
2020-05-14 18:41:36 -04:00
e98b8801f3 Merge pull request #483 from sbwalker/master
removed redundant assembly download logic, added security on download controller methods
2020-05-14 18:41:15 -04:00
caabac3e74 removed redundant assembly download logic, added security on download controller methods 2020-05-14 18:40:53 -04:00
422f360807 disabled changed to readonly 2020-05-14 14:58:09 -07:00
6e28fa47a2 container class added to pane
resolves issue with title border DIV not utilizing 100% pane size
2020-05-14 14:03:09 -07:00
b4f3c4ae56 Merge pull request #22 from oqtane/master
sync
2020-05-14 14:22:15 -04:00
bafe2c6666 fix module creator templates 2020-05-14 14:22:11 -04:00
5eec442442 Merge pull request #479 from sbwalker/master
fix module creator templates
2020-05-14 14:21:28 -04:00
def12489e6 Merge pull request #8 from oqtane/master
Sync master
2020-05-14 12:12:26 -04:00
82429c2545 Merge pull request #477 from jimspillane/PathTraversal
Add File and Path rules
2020-05-14 11:55:07 -04:00
aa97dd4d0d Allow modules to be installed/uninstalled with embedded scripts or IInstallable interface. Fix module uninstall issues. 2020-05-14 11:54:28 -04:00
cba5865e81 Merge pull request #478 from sbwalker/master
Allow modules to be installed/uninstalled with embedded scripts or IInstallable interface. Fix module uninstall issues.
2020-05-14 11:53:33 -04:00
8afe8e7474 Add File and Path rules
Apply the file and path naming rules found at 
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file

Mitigate path traversal.
2020-05-14 09:53:36 -04:00
a9630e715b Merge pull request #7 from oqtane/master
Sync upstream master
2020-05-14 08:43:53 -04:00
6824b3f1b5 Merge pull request #471 from jimspillane/ReservedNameCheck
Add additional reserved names and characters
2020-05-13 16:25:27 -04:00
1cca18c4d2 Add additional reserved names and characters
Added CONIN$,CONOUT$ and characters <>:"/\|?*

Added .Split('.')[0] to folder.Name to catch names like CON.txt and allow names like CONTRACT.
2020-05-12 22:38:28 -04:00
a886ae12cc Merge pull request #6 from oqtane/master
Sync master
2020-05-12 20:52:07 -04:00
560c995564 Include AliasId in service API calls ( this is not needed for interacting with the MasterDB repository however it is needed for tenant-based logging ) 2020-05-12 20:31:31 -04:00
69a5077eda Merge pull request #470 from sbwalker/master
Include AliasId in service API calls ( this is not needed for interacting with the MasterDB repository however it is needed for tenant-based logging )
2020-05-12 20:30:27 -04:00
3efd39c74f Merge pull request #21 from oqtane/master
sync
2020-05-12 20:08:55 -04:00
bbcb054f7a Merge pull request #469 from chlupac/IClientStartup
Client assembly selection criteria changed
2020-05-12 20:07:27 -04:00
88cf30f7c6 Merge branch 'master' of https://github.com/oqtane/oqtane.framework into IClientStartup 2020-05-12 23:20:06 +02:00
4c188b782d ClientAssembly selection criteria changed 2020-05-12 23:17:37 +02:00
196e3d5865 Merge pull request #468 from sbwalker/master
updated module creator templates to use dependency injection in module components
2020-05-12 16:38:47 -04:00
6fd0efbb73 updated module creator templates to use dependency injection in module components 2020-05-12 16:31:53 -04:00
9eec0fd86b updated HtmlText module to use dependency injection 2020-05-12 15:04:07 -04:00
ad70128747 Merge pull request #466 from sbwalker/master
updated HtmlText module to use dependency injection
2020-05-12 15:04:01 -04:00
ee55b4e3cf Merge pull request #465 from sbwalker/master
add security to site template API
2020-05-12 14:38:52 -04:00
53f454e370 Merge pull request #20 from oqtane/master
sync
2020-05-12 14:37:37 -04:00
f05c7d79e3 add security to site template API 2020-05-12 14:31:18 -04:00
598b433cd2 Merge pull request #463 from chlupac/IClientStartup
IClientStartup  implementation
2020-05-12 14:31:10 -04:00
6ac4ba4617 Merge pull request #462 from mikecasas/patch-1
Update IModuleControl.cs
2020-05-12 14:30:59 -04:00
f4710f90c0 Merge pull request #464 from sbwalker/master
validate folder names, handle missing files more gracefully
2020-05-12 14:30:44 -04:00
6f3fe8d933 validate folder names, handle missing files more gracefully 2020-05-12 13:24:51 -04:00
da73d519d7 IClientStartup implementation 2020-05-12 10:00:28 +02:00
7f157582cc Update IModuleControl.cs
Added additional comments.
2020-05-11 13:47:12 -04:00
4d7ec16f36 Merge pull request #459 from chlupac/IServerStartup
IServerStartup implementation
2020-05-11 12:56:22 -04:00
7c814a67b3 IServerStartup implementation 2020-05-11 11:19:12 +02:00
c83496d814 Merge pull request #19 from oqtane/master
sync
2020-05-10 10:03:50 -04:00
83d47376cc Merge pull request #458 from jimspillane/FixContainsFiles
Fix contains files
2020-05-10 09:43:59 -04:00
a1449fb2dd Fix Uploadable files
When testing for allowable file extensions using a comma separated list, like (jpg,mp3,txt,zip), extensions such as .xt or .p3 will return true.  Adding Split(',') will test each of the extensions correctly.

Adding ToLower() will allow mixed case extensions, like .JPG or .Zip to return true.
2020-05-09 14:58:39 -04:00
2362faaee1 Merge pull request #5 from oqtane/master
Sync master
2020-05-09 14:04:36 -04:00
7ca1f92f52 Merge pull request #4 from oqtane/master
Sync master
2020-05-05 21:26:18 -04:00
c30ee60433 Merge pull request #3 from oqtane/master
Sync master
2020-05-01 18:07:34 -04:00
e627e14233 Merge pull request #2 from oqtane/master
Sync master
2020-05-01 14:50:51 -04:00
4a6b12b6f9 Merge pull request #1 from oqtane/master
Sync master
2020-04-30 21:07:21 -04:00
123 changed files with 12553 additions and 808 deletions

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Admin.Files
@using System.IO
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IFileService FileService
@ -12,7 +13,7 @@
<Label For="upload" HelpText="Upload the file you want">Upload: </Label>
</td>
<td>
<FileManager UploadMultiple="true" ShowFiles="false" FolderId="@_folderId.ToString()" />
<FileManager UploadMultiple="true" ShowFiles="false" FolderId="@_folderId" />
</td>
</tr>
</table>
@ -70,18 +71,32 @@
private async Task Download()
{
if (url == string.Empty || _folderId == -1)
{
AddModuleMessage("You Must Enter A Url And Select A Folder", MessageType.Warning);
return;
}
var filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
if (!Constants.UploadableFiles.Split(',')
.Contains(Path.GetExtension(filename).ToLower().Replace(".", "")))
{
AddModuleMessage("File Could Not Be Downloaded From Url Due To Its File Extension", MessageType.Warning);
return ;
}
if (!filename.IsPathOrFileValid())
{
AddModuleMessage("You Must Enter A Url With A Valid File Name", MessageType.Warning);
return;
}
try
{
if (url != string.Empty && _folderId != -1)
{
await FileService.UploadFileAsync(url, _folderId);
await logger.LogInformation("File Downloaded Successfully From Url {Url}", url);
AddModuleMessage("File Downloaded Successfully From Url", MessageType.Success);
}
else
{
AddModuleMessage("You Must Enter A Url And Select A Folder", MessageType.Warning);
}
await FileService.UploadFileAsync(url, _folderId);
await logger.LogInformation("File Downloaded Successfully From Url {Url}", url);
AddModuleMessage("File Downloaded Successfully From Url", MessageType.Success);
}
catch (Exception ex)
{

View File

@ -25,7 +25,7 @@
</tr>
<tr>
<td>
<Label for="name" HelpText="Enter the file name">Name: </Label>
<Label for="name" HelpText="Enter the folder name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
@ -112,51 +112,63 @@
private async Task SaveFolder()
{
if (_name == string.Empty || _parentId == -1)
{
AddModuleMessage("Folders Must Have A Parent And A Name", MessageType.Warning);
return;
}
if (!_name.IsPathOrFileValid())
{
AddModuleMessage("Folder Name Not Valid.", MessageType.Warning);
return;
}
try
{
if (_name != string.Empty && _parentId != -1)
Folder folder;
if (_folderId != -1)
{
Folder folder;
if (_folderId != -1)
{
folder = await FolderService.GetFolderAsync(_folderId);
}
else
{
folder = new Folder();
}
folder = await FolderService.GetFolderAsync(_folderId);
}
else
{
folder = new Folder();
}
folder.SiteId = PageState.Site.SiteId;
folder.SiteId = PageState.Site.SiteId;
if (_parentId == -1)
{
folder.ParentId = null;
}
else
{
folder.ParentId = _parentId;
}
if (_parentId == -1)
{
folder.ParentId = null;
}
else
{
folder.ParentId = _parentId;
}
folder.Name = _name;
folder.IsSystem = _isSystem;
folder.Permissions = _permissionGrid.GetPermissions();
folder.Name = _name;
folder.IsSystem = _isSystem;
folder.Permissions = _permissionGrid.GetPermissions();
if (_folderId != -1)
{
folder = await FolderService.UpdateFolderAsync(folder);
}
else
{
folder = await FolderService.AddFolderAsync(folder);
}
if (_folderId != -1)
{
folder = await FolderService.UpdateFolderAsync(folder);
}
else
{
folder = await FolderService.AddFolderAsync(folder);
}
if (folder != null)
{
await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId);
await logger.LogInformation("Folder Saved {Folder}", folder);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage("Folders Must Have A Parent And A Name", MessageType.Warning);
AddModuleMessage("An Error Was Encountered Saving The Folder", MessageType.Error);
}
}
catch (Exception ex)

View File

@ -3,53 +3,66 @@
@inject NavigationManager NavigationManager
@inject IModuleDefinitionService ModuleDefinitionService
@inject IModuleService ModuleService
@inject ISystemService SystemService
<table class="table table-borderless">
<table class="table table-borderless">
<tr>
<td>
<Label For="owner" HelpText="Enter the name of the organization who is developing this module. It should not contain spaces or punctuation.">Owner Name: </Label>
</td>
<td>
<input id="owner" class="form-control" @bind="@_owner" />
</td>
</tr>
<tr>
<td>
<Label For="module" HelpText="Enter a name for this module. It should be in singular form (ie. Car) and not contain spaces or punctuation.">Module Name: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_module" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="Enter s short description for the module">Description: </Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="3"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="template" HelpText="Select a module template. Internal modules are created inside of the Oqtane solution. External modules are created outside of the Oqtane solution.">Template: </Label>
</td>
<td>
<select id="template" class="form-control" @onchange="(e => TemplateChanged(e))">
<option value="-">&lt;Select Template&gt;</option>
<option value="internal">Internal</option>
<option value="external">External</option>
</select>
</td>
</tr>
@if (!string.IsNullOrEmpty(_location))
{
<tr>
<td>
<Label For="owner" HelpText="Enter the name of the organization who is developing this module. It should not contain spaces or punctuation.">Owner Name: </Label>
<Label For="location" HelpText="Location where the module will be created">Location: </Label>
</td>
<td>
<input id="owner" class="form-control" @bind="@_owner" />
<input id="module" class="form-control" @bind="@_location" readonly />
</td>
</tr>
<tr>
<td>
<Label For="module" HelpText="Enter a name for this module. It should be in singular form (ie. Car) and not contain spaces or punctuation.">Module Name: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_module" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="Enter s short description for the module">Description: </Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="3"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="template" HelpText="Select a module template. Internal modules are created inside of the Oqtane solution. External modules are created outside of the Oqtane solution.">Template: </Label>
</td>
<td>
<select id="template" class="form-control" @bind="@_template">
<option value="">&lt;Select Template&gt;</option>
<option value="internal">Internal</option>
<option value="external">External</option>
</select>
</td>
</tr>
</table>
}
</table>
<button type="button" class="btn btn-success" @onclick="CreateModule">Create Module</button>
@code {
@code {
private string _owner = string.Empty;
private string _module = string.Empty;
private string _description = string.Empty;
private string _template = string.Empty;
private string _template = "-";
private string _location = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -62,9 +75,9 @@
{
try
{
if (!string.IsNullOrEmpty(_owner) && !string.IsNullOrEmpty(_module) && !string.IsNullOrEmpty(_template))
if (!string.IsNullOrEmpty(_owner) && !string.IsNullOrEmpty(_module) && _template != "-")
{
var moduleDefinition = new ModuleDefinition { Owner = _owner.Replace(" ",""), Name = _module.Replace(" ", ""), Description = _description, Template = _template };
var moduleDefinition = new ModuleDefinition { Owner = _owner.Replace(" ", ""), Name = _module.Replace(" ", ""), Description = _description, Template = _template };
await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition, ModuleState.ModuleId);
}
else
@ -77,4 +90,35 @@
await logger.LogError(ex, "Error Creating Module");
}
}
private async void TemplateChanged(ChangeEventArgs e)
{
try
{
_location = string.Empty;
_template = (string)e.Value;
if (_template != "-")
{
Dictionary<string, string> systeminfo = await SystemService.GetSystemInfoAsync();
if (systeminfo != null)
{
string[] path = systeminfo["serverpath"].Split('\\');
if (_template == "internal")
{
_location = string.Join("\\", path, 0, path.Length - 1) + "\\Oqtane.Client\\Modules\\" + _owner + "." + _module + "s";
}
else
{
_location = string.Join("\\", path, 0, path.Length - 2) + "\\" + _owner + "." + _module + "s";
}
}
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting System Info {Error}", ex.Message);
AddModuleMessage("Error Getting System Info", MessageType.Error);
}
}
}

View File

@ -2,11 +2,10 @@
@using [Owner].[Module]s.Services
@using [Owner].[Module]s.Models
@namespace [Owner].[Module]s.Modules
@namespace [Owner].[Module]s
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject NavigationManager NavigationManager
@inject HttpClient http
@inject SiteState sitestate
<table class="table table-borderless">
<tr>
@ -29,9 +28,14 @@
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit";
I[Module]Service [Module]Service;
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
};
int _id;
string _name;
string _createdby;
@ -43,7 +47,6 @@
{
try
{
[Module]Service = new [Module]Service(http, sitestate);
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);

View File

@ -1,11 +1,10 @@
@using [Owner].[Module]s.Services
@using [Owner].[Module]s.Models
@namespace [Owner].[Module]s.Modules
@namespace [Owner].[Module]s
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject NavigationManager NavigationManager
@inject HttpClient http
@inject SiteState sitestate
@if (_[Module]s == null)
{
@ -62,8 +61,9 @@ else
- Repository\I[Module]Repository.cs - interface for defining repository methods<br />
- Repository\[Module]Respository.cs - implements repository interface methods for data access using EF Core<br />
- Repository\[Module]Context.cs - provides a DB Context for data access<br />
- Scripts\[Owner].[Module].1.0.0.sql - database schema definition script<br /><br />
- Scripts\[Owner].[Module].Uninstall.sql - database uninstall script<br /><br />
- Scripts\[Owner].[Module]s.1.0.0.sql - database schema definition script<br />
- Scripts\[Owner].[Module]s.Uninstall.sql - database uninstall script<br />
- wwwroot\Module.css - module style sheet<br /><br />
[RootPath]Shared\<br />
- [Owner].[Module]s.csproj - shared project<br />
- Models\[Module].cs - model definition<br /><br />
@ -71,14 +71,17 @@ else
<!-- The content above is for informational purposes only and can be safely removed -->
@code {
I[Module]Service [Module]Service;
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
};
List<[Module]> _[Module]s;
protected override async Task OnInitializedAsync()
{
try
{
[Module]Service = new [Module]Service(http, sitestate);
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
}
catch (Exception ex)

View File

@ -1,7 +1,7 @@
using Oqtane.Models;
using Oqtane.Modules;
namespace [Owner].[Module]s.Modules
namespace [Owner].[Module]s
{
public class ModuleInfo : IModule
{
@ -10,7 +10,6 @@ namespace [Owner].[Module]s.Modules
Name = "[Module]",
Description = "[Module]",
Version = "1.0.0",
Dependencies = "[Owner].[Module]s.Shared.Oqtane",
ServerManagerType = "[ServerManagerType]",
ReleaseVersions = "1.0.0"
};

View File

@ -1,4 +1,4 @@
@namespace [Owner].[Module]s.Modules
@namespace [Owner].[Module]s
@inherits ModuleBase
@inject ISettingService SettingService

View File

@ -15,4 +15,6 @@
@using Oqtane.Services
@using Oqtane.Shared
@using Oqtane.Themes
@using Oqtane.Themes.Controls
@using Oqtane.Themes.Controls
@using Oqtane.UI
@using Oqtane.Enums

View File

@ -26,6 +26,7 @@
<file src="..\Server\bin\Release\netcoreapp3.1\[Owner].[Module]s.Server.Oqtane.pdb" target="lib" />
<file src="..\Shared\bin\Release\netstandard2.1\[Owner].[Module]s.Shared.Oqtane.dll" target="lib" />
<file src="..\Shared\bin\Release\netstandard2.1\[Owner].[Module]s.Shared.Oqtane.pdb" target="lib" />
<file src="..\wwwroot\**\*.*" target="wwwroot" />
<file src="..\Server\wwwroot\**\*.*" target="wwwroot" />
<file src="..\Server\content\**\*.*" target="content" />
</files>
</package>

View File

@ -23,12 +23,12 @@ namespace [Owner].[Module]s.Manager
public bool Install(Tenant tenant, string version)
{
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]." + version + ".sql");
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]s." + version + ".sql");
}
public bool Uninstall(Tenant tenant)
{
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module].Uninstall.sql");
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]s.Uninstall.sql");
}
public string ExportModule(Module module)

View File

@ -14,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Scripts\[Owner].[Module].1.0.0.sql" />
<EmbeddedResource Include="Scripts\[Owner].[Module].Uninstall.sql" />
<EmbeddedResource Include="Scripts\[Owner].[Module]s.1.0.0.sql" />
<EmbeddedResource Include="Scripts\[Owner].[Module]s.Uninstall.sql" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1 @@
This is the location where static resources for third party libraries should be located ( the third party library assemblies will be included in the /lib folder ). They should be placed in subfolders which match the naming convention of the third party library. When the module package is deployed the static resource subfolders will be extracted under the web root.

View File

@ -0,0 +1 @@
/* Module Custom Styles */

View File

@ -0,0 +1 @@
This is the location where static resources such as images, style sheets, or scripts for this module will be located. Static assets can be organized in subfolders. When the module package is deployed the assets will be extracted under the web root in a folder that matches the module namespace.

View File

@ -2,11 +2,10 @@
@using [Owner].[Module]s.Services
@using [Owner].[Module]s.Models
@namespace [Owner].[Module]s.Modules
@namespace [Owner].[Module]s
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject NavigationManager NavigationManager
@inject HttpClient http
@inject SiteState sitestate
<table class="table table-borderless">
<tr>
@ -31,7 +30,6 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit";
I[Module]Service [Module]Service;
int _id;
string _name;
string _createdby;
@ -43,7 +41,6 @@
{
try
{
[Module]Service = new [Module]Service(http, sitestate);
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);

View File

@ -1,11 +1,10 @@
@using [Owner].[Module]s.Services
@using [Owner].[Module]s.Models
@namespace [Owner].[Module]s.Modules
@namespace [Owner].[Module]s
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject NavigationManager NavigationManager
@inject HttpClient http
@inject SiteState sitestate
@if (_[Module]s == null)
{
@ -54,22 +53,20 @@ else
- Repository\I[Module]Repository.cs - interface for defining repository methods<br />
- Repository\[Module]Respository.cs - implements repository interface methods for data access using EF Core<br />
- Repository\[Module]Context.cs - provides a DB Context for data access<br />
- Scripts\[Owner].[Module].1.0.0.sql - database schema definition script<br /><br />
- Scripts\[Owner].[Module].Uninstall.sql - database uninstall script<br /><br />
- Scripts\[Owner].[Module]s.1.0.0.sql - database schema definition script<br />
- Scripts\[Owner].[Module]s.Uninstall.sql - database uninstall script<br /><br />
[RootPath]Oqtane.Shared\Modules\[Module]\<br />
- Models\[Module].cs - model definition<br /><br />
<!-- The content above is for informational purposes only and can be safely removed -->
@code {
I[Module]Service [Module]Service;
List<[Module]> _[Module]s;
protected override async Task OnInitializedAsync()
{
try
{
[Module]Service = new [Module]Service(http, sitestate);
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
}
catch (Exception ex)

View File

@ -1,7 +1,7 @@
using Oqtane.Models;
using Oqtane.Modules;
namespace [Owner].[Module]s.Modules
namespace [Owner].[Module]s
{
public class ModuleInfo : IModule
{
@ -10,7 +10,6 @@ namespace [Owner].[Module]s.Modules
Name = "[Module]",
Description = "[Module]",
Version = "1.0.0",
Dependencies = "[Owner].[Module]s.Module.Shared",
ServerManagerType = "[ServerManagerType]",
ReleaseVersions = "1.0.0"
};

View File

@ -1,4 +1,4 @@
@namespace [Owner].[Module]s.Modules
@namespace [Owner].[Module]s
@inherits ModuleBase
@inject ISettingService SettingService

View File

@ -10,25 +10,13 @@ using [Owner].[Module]s.Repository;
namespace [Owner].[Module]s.Manager
{
public class [Module]Manager : IInstallable, IPortable
public class [Module]Manager : IPortable
{
private I[Module]Repository _[Module]s;
private ISqlRepository _sql;
public [Module]Manager(I[Module]Repository [Module]s, ISqlRepository sql)
public [Module]Manager(I[Module]Repository [Module]s)
{
_[Module]s = [Module]s;
_sql = sql;
}
public bool Install(Tenant tenant, string version)
{
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]." + version + ".sql");
}
public bool Uninstall(Tenant tenant)
{
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module].Uninstall.sql");
}
public string ExportModule(Module module)

View File

@ -35,7 +35,7 @@
<Label HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation.">Module: </Label>
</td>
<td>
<FileManager Filter="nupkg" ShowFiles="false" Folder="Modules" UploadMultiple="True" />
<FileManager Filter="nupkg" ShowFiles="false" Folder="Modules" UploadMultiple="true" />
</td>
</tr>
</table>

View File

@ -24,7 +24,7 @@
</td>
<td>
<select id="container" class="form-control" @bind="@_containerType">
<option value="">&lt;Select Container&gt;</option>
<option value="-">&lt;Inherit From Page Or Site&gt;</option>
@foreach (KeyValuePair<string, string> container in _containers)
{
<option value="@container.Key">@container.Value</option>
@ -107,6 +107,14 @@
_title = ModuleState.Title;
_containers = ThemeService.GetContainerTypes(await ThemeService.GetThemesAsync());
_containerType = ModuleState.ContainerType;
if (!string.IsNullOrEmpty(PageState.Page.DefaultContainerType) && _containerType == PageState.Page.DefaultContainerType)
{
_containerType = "-";
}
if (_containerType == PageState.Site.DefaultContainerType)
{
_containerType = "-";
}
_allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.Permissions;
_permissionNames = ModuleState.ModuleDefinition.PermissionNames;
@ -115,8 +123,8 @@
_settingsModuleType = Type.GetType(ModuleState.ModuleType);
if (_settingsModuleType != null)
{
var moduleobject = Activator.CreateInstance(_settingsModuleType);
_settingstitle = (string)_settingsModuleType.GetProperty("Title").GetValue(moduleobject, null);
var moduleobject = Activator.CreateInstance(_settingsModuleType) as IModuleControl;
_settingstitle = moduleobject.Title;
if (string.IsNullOrEmpty(_settingstitle))
{
_settingstitle = "Other Settings";
@ -136,7 +144,15 @@
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title;
pagemodule.ContainerType = _containerType;
pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty;
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType)
{
pagemodule.ContainerType = string.Empty;
}
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Site.DefaultContainerType)
{
pagemodule.ContainerType = string.Empty;
}
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);

View File

@ -101,7 +101,7 @@
</td>
<td>
<select id="Theme" class="form-control" @onchange="(e => ThemeChanged(e))">
<option value="-">&lt;Select Theme&gt;</option>
<option value="-">&lt;Inherit From Site&gt;</option>
@foreach (KeyValuePair<string, string> item in _themes)
{
if (item.Key == _themetype)
@ -122,7 +122,7 @@
</td>
<td>
<select id="Layout" class="form-control" @bind="@_layouttype">
<option value="-">&lt;Select Layout&gt;</option>
<option value="-">&lt;Inherit From Site&gt;</option>
@foreach (KeyValuePair<string, string> panelayout in _panelayouts)
{
if (panelayout.Key == _layouttype)
@ -137,6 +137,20 @@
</select>
</td>
</tr>
<tr>
<td>
<Label For="defaultContainer" HelpText="Select the default container for the page">Default Container: </Label>
</td>
<td>
<select id="defaultContainer" class="form-control" @bind="@_containertype">
<option value="-">&lt;Inherit From Site&gt;</option>
@foreach (KeyValuePair<string, string> container in _containers)
{
<option value="@container.Key">@container.Value</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="Icon" HelpText="Optionally provide an icon for this page which will be displayed in the site navigation">Icon: </Label>
@ -187,6 +201,7 @@
@code {
private Dictionary<string, string> _themes;
private Dictionary<string, string> _panelayouts;
private Dictionary<string, string> _containers = new Dictionary<string, string>();
private List<Theme> _themeList;
private List<Page> _pageList;
private string _name;
@ -202,6 +217,7 @@
private string _mode = "view";
private string _themetype = "-";
private string _layouttype = "-";
private string _containertype = "-";
private string _icon = string.Empty;
private string _permissions = string.Empty;
private PermissionGrid _permissionGrid;
@ -216,11 +232,9 @@
_pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_themetype = PageState.Site.DefaultThemeType;
_layouttype = PageState.Site.DefaultLayoutType;
_themes = ThemeService.GetThemeTypes(_themeList);
_panelayouts = ThemeService.GetPaneLayoutTypes(_themeList, _themetype);
_containers = ThemeService.GetContainerTypes(_themeList);
_permissions = string.Empty;
}
@ -351,16 +365,20 @@
page.Url = _url;
page.EditMode = (_mode == "edit" ? true : false);
page.ThemeType = (_themetype != "-") ? _themetype : string.Empty;
page.LayoutType = (_layouttype != "-") ? _layouttype : string.Empty;
if (page.ThemeType == PageState.Site.DefaultThemeType)
if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType)
{
page.ThemeType = string.Empty;
}
if (page.LayoutType == PageState.Site.DefaultLayoutType)
page.LayoutType = (_layouttype != "-") ? _layouttype : string.Empty;
if (!string.IsNullOrEmpty(page.LayoutType) && page.LayoutType == PageState.Site.DefaultLayoutType)
{
page.LayoutType = string.Empty;
}
page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty;
if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType)
{
page.DefaultContainerType = string.Empty;
}
page.Icon = (_icon == null ? string.Empty : _icon);
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable));

View File

@ -112,7 +112,7 @@
</td>
<td>
<select id="Theme" class="form-control" @onchange="(e => ThemeChanged(e))">
<option value="-">&lt;Select Theme&gt;</option>
<option value="-">&lt;Inherit From Site&gt;</option>
@foreach (KeyValuePair<string, string> item in _themes)
{
if (item.Key == _themetype)
@ -133,7 +133,7 @@
</td>
<td>
<select id="Layout" class="form-control" @bind="@_layouttype">
<option value="-">&lt;Select Layout&gt;</option>
<option value="-">&lt;Inherit From Site&gt;</option>
@foreach (KeyValuePair<string, string> panelayout in _panelayouts)
{
if (panelayout.Key == _layouttype)
@ -148,6 +148,20 @@
</select>
</td>
</tr>
<tr>
<td>
<Label For="defaultContainer" HelpText="Select the default container for the page">Default Container: </Label>
</td>
<td>
<select id="defaultContainer" class="form-control" @bind="@_containertype">
<option value="-">&lt;Inherit From Site&gt;</option>
@foreach (KeyValuePair<string, string> container in _containers)
{
<option value="@container.Key">@container.Value</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="Icon" HelpText="Optionally provide an icon for this page which will be displayed in the site navigation">Icon: </Label>
@ -200,6 +214,7 @@
@code {
private Dictionary<string, string> _themes;
private Dictionary<string, string> _panelayouts;
private Dictionary<string, string> _containers = new Dictionary<string, string>();
private List<Theme> _themeList;
private List<Page> _pageList;
private int _pageId;
@ -217,6 +232,7 @@
private string _mode;
private string _themetype = "-";
private string _layouttype = "-";
private string _containertype = "-";
private string _icon;
private string _permissions;
private string _createdby;
@ -241,6 +257,7 @@
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_themes = ThemeService.GetThemeTypes(_themeList);
_containers = ThemeService.GetContainerTypes(_themeList);
_pageId = Int32.Parse(PageState.QueryString["id"]);
var page = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId);
@ -270,16 +287,21 @@
_ispersonalizable = page.IsPersonalizable.ToString();
_mode = (page.EditMode) ? "edit" : "view";
_themetype = page.ThemeType;
_panelayouts = ThemeService.GetPaneLayoutTypes(_themeList, _themetype);
_layouttype = page.LayoutType;
if (_themetype == PageState.Site.DefaultThemeType)
{
_themetype = "-";
}
_panelayouts = ThemeService.GetPaneLayoutTypes(_themeList, _themetype);
_layouttype = page.LayoutType;
if (_layouttype == PageState.Site.DefaultLayoutType)
{
_layouttype = "-";
}
_containertype = page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype))
{
_containertype = "-";
}
_icon = page.Icon;
_permissions = page.Permissions;
_createdby = page.CreatedBy;
@ -426,15 +448,20 @@
page.Url = _url;
page.EditMode = (_mode == "edit");
page.ThemeType = (_themetype != "-") ? _themetype : string.Empty;
page.LayoutType = (_layouttype != "-") ? _layouttype : string.Empty;
if (page.ThemeType == PageState.Site.DefaultThemeType)
if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType)
{
page.ThemeType = string.Empty;
}
if (page.LayoutType == PageState.Site.DefaultLayoutType)
page.LayoutType = (_layouttype != "-") ? _layouttype : string.Empty;
if (!string.IsNullOrEmpty(page.LayoutType) && page.LayoutType == PageState.Site.DefaultLayoutType)
{
page.LayoutType = string.Empty;
}
page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty;
if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType)
{
page.DefaultContainerType = string.Empty;
}
page.Icon = _icon ?? string.Empty;
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable));

View File

@ -39,7 +39,7 @@
<Label For="logo" HelpText="Upload a logo for the site">Logo: </Label>
</td>
<td>
<FileManager FileId="@_logofileid.ToString()" Filter="@Constants.ImageFiles" @ref="_logofilemanager" />
<FileManager FileId="@_logofileid" Filter="@Constants.ImageFiles" @ref="_logofilemanager" />
</td>
</tr>
<tr>
@ -47,7 +47,7 @@
<Label For="favicon" HelpText="Select Your default icon">Favicon: </Label>
</td>
<td>
<FileManager FileId="@_faviconfileid.ToString()" Filter="ico" @ref="_faviconfilemanager" />
<FileManager FileId="@_faviconfileid" Filter="ico" @ref="_faviconfilemanager" />
</td>
</tr>
<tr>
@ -185,7 +185,7 @@
<Label For="appIcon" HelpText="Include an application icon for your PWA. It should be a PNG which is 192 X 192 pixels in dimension.">App Icon: </Label>
</td>
<td>
<FileManager FileId="@_pwaappiconfileid.ToString()" Filter="png" @ref="_pwaappiconfilemanager" />
<FileManager FileId="@_pwaappiconfileid" Filter="png" @ref="_pwaappiconfilemanager" />
</td>
</tr>
<tr>
@ -193,7 +193,7 @@
<Label For="splashIcon" HelpText="Include a splash icon for your PWA. It should be a PNG which is 512 X 512 pixels in dimension.">Splash Icon: </Label>
</td>
<td>
<FileManager FileId="@_pwasplashiconfileid.ToString()" Filter="png" @ref="_pwasplashiconfilemanager" />
<FileManager FileId="@_pwasplashiconfileid" Filter="png" @ref="_pwasplashiconfilemanager" />
</td>
</tr>
</table>

View File

@ -8,7 +8,7 @@
<Label For="version" HelpText="Framework Version">Framework Version: </Label>
</td>
<td>
<input id="version" class="form-control" @bind="@_version" disabled />
<input id="version" class="form-control" @bind="@_version" readonly />
</td>
</tr>
<tr>
@ -16,7 +16,7 @@
<Label For="runtime" HelpText="Blazor Runtime (Server or WebAssembly)">Blazor Runtime: </Label>
</td>
<td>
<input id="runtime" class="form-control" @bind="@_runtime" disabled />
<input id="runtime" class="form-control" @bind="@_runtime" readonly />
</td>
</tr>
<tr>
@ -24,7 +24,7 @@
<Label For="clrversion" HelpText="Common Language Runtime Version">CLR Version: </Label>
</td>
<td>
<input id="clrversion" class="form-control" @bind="@_clrversion" disabled />
<input id="clrversion" class="form-control" @bind="@_clrversion" readonly />
</td>
</tr>
<tr>
@ -32,7 +32,7 @@
<Label For="osversion" HelpText="Operating System Version">OS Version: </Label>
</td>
<td>
<input id="osversion" class="form-control" @bind="@_osversion" disabled />
<input id="osversion" class="form-control" @bind="@_osversion" readonly />
</td>
</tr>
<tr>
@ -40,7 +40,7 @@
<Label For="serverpath" HelpText="Server Path">Server Path: </Label>
</td>
<td>
<input id="serverpath" class="form-control" @bind="@_serverpath" disabled />
<input id="serverpath" class="form-control" @bind="@_serverpath" readonly />
</td>
</tr>
<tr>
@ -48,7 +48,7 @@
<Label For="servertime" HelpText="Server Time">Server Time: </Label>
</td>
<td>
<input id="servertime" class="form-control" @bind="@_servertime" disabled />
<input id="servertime" class="form-control" @bind="@_servertime" readonly />
</td>
</tr>
</table>

View File

@ -35,7 +35,7 @@
<Label HelpText="Upload one or more theme packages. Once they are uploaded click Install to complete the installation.">Theme: </Label>
</td>
<td>
<FileManager Filter="nupkg" ShowFiles="false" Folder="Themes" UploadMultiple="True" />
<FileManager Filter="nupkg" ShowFiles="false" Folder="Themes" UploadMultiple="@true" />
</td>
</tr>
</table>

View File

@ -64,7 +64,7 @@ else
<label for="Name" class="control-label">Photo: </label>
</td>
<td>
<FileManager FileId="@photofileid.ToString()" @ref="filemanager" />
<FileManager FileId="@photofileid" @ref="filemanager" />
</td>
</tr>
</table>

View File

@ -63,7 +63,7 @@ else
<label class="control-label">Photo: </label>
</td>
<td>
<FileManager FileId="@photofileid.ToString()" @ref="filemanager" />
<FileManager FileId="@photofileid" @ref="filemanager" />
</td>
</tr>
<tr>

View File

@ -62,9 +62,9 @@
[Parameter]
public string Class { get; set; } // optional
[Parameter]
public bool Disabled { get; set; } // optional
[Parameter]
public bool Disabled { get; set; } // optional
[Parameter]
public string EditMode { get; set; } // optional - specifies if a user must be in edit mode to see the action - default is true
@ -109,8 +109,8 @@
Type moduleType = Type.GetType(typename);
if (moduleType != null)
{
var moduleobject = Activator.CreateInstance(moduleType);
security = (SecurityAccessLevel)moduleType.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
var moduleobject = Activator.CreateInstance(moduleType) as IModuleControl;
security = moduleobject.SecurityAccessLevel;
}
else
{

View File

@ -101,8 +101,8 @@
var moduleType = Type.GetType(typename);
if (moduleType != null)
{
var moduleobject = Activator.CreateInstance(moduleType);
security = (SecurityAccessLevel)moduleType.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
var moduleobject = Activator.CreateInstance(moduleType) as IModuleControl;
security = moduleobject.SecurityAccessLevel;
}
else
{

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleBase
@inherits ModuleBase
@attribute [OqtaneIgnore]
@inject IFolderService FolderService
@inject IFileService FileService
@ -10,33 +11,36 @@
<div id="@Id" class="container-fluid px-0">
<div class="row">
<div class="col">
<div>
<select class="form-control" @onchange="(e => FolderChanged(e))">
@if (string.IsNullOrEmpty(Folder))
{
<option value="-1">&lt;Select Folder&gt;</option>
}
@foreach (Folder folder in _folders)
{
if (folder.FolderId == _folderid)
@if (ShowFolders || FolderId <= 0)
{
<div>
<select class="form-control" @onchange="(e => FolderChanged(e))">
@if (string.IsNullOrEmpty(Folder))
{
<option value="@(folder.FolderId)" selected>@(new string('-', folder.Level * 2))@(folder.Name)</option>
<option value="-1">&lt;Select Folder&gt;</option>
}
else
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
if (folder.FolderId == FolderId)
{
<option value="@(folder.FolderId)" selected>@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
else
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
}
}
</select>
</div>
@if (_showfiles)
</select>
</div>
}
@if (ShowFiles)
{
<div>
<select class="form-control" @onchange="(e => FileChanged(e))">
<option value="-1">&lt;Select File&gt;</option>
@foreach (File file in _files)
{
if (file.FileId == _fileid)
if (file.FileId == FileId)
{
<option value="@(file.FileId)" selected>@(file.Name)</option>
}
@ -48,33 +52,33 @@
</select>
</div>
}
@if (_haseditpermission)
@if (ShowUpload && _haseditpermission)
{
<div>
@if (_uploadmultiple)
@if (UploadMultiple)
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" multiple />
<input type="file" id="@_fileinputid" name="file" accept="@_filter" multiple/>
}
else
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" />
<input type="file" id="@_fileinputid" name="file" accept="@_filter"/>
}
<span id="@_progressinfoid"></span><progress id="@_progressbarid" style="width: 150px; visibility: hidden;"></progress>
<span class="float-right">
<button type="button" class="btn btn-success" @onclick="UploadFile">Upload</button>
@if (_showfiles && GetFileId() != -1)
{
<button type="button" class="btn btn-danger" @onclick="DeleteFile">Delete</button>
}
<button type="button" class="btn btn-success" @onclick="UploadFile">Upload</button>
@if (_showfiles && GetFileId() != -1)
{
<button type="button" class="btn btn-danger" @onclick="DeleteFile">Delete</button>
}
</span>
</div>
@((MarkupString)_message)
}
@((MarkupString) _message)
</div>
@if (_image != string.Empty)
{
<div class="col-auto">
@((MarkupString)_image)
@((MarkupString) _image)
</div>
}
</div>
@ -84,15 +88,12 @@
@code {
private string _id;
private List<Folder> _folders;
private int _folderid = -1;
private List<File> _files = new List<File>();
private int _fileid = -1;
private bool _showfiles = true;
private string _fileinputid = string.Empty;
private string _progressinfoid = string.Empty;
private string _progressbarid = string.Empty;
private string _filter = "*";
private bool _uploadmultiple = false;
private bool _haseditpermission = false;
private string _message = string.Empty;
private string _image = string.Empty;
@ -105,19 +106,25 @@
public string Folder { get; set; } // optional - for setting a specific folder by default
[Parameter]
public string FolderId { get; set; } // optional - for setting a specific folderid by default
public int FolderId { get; set; } = -1; // optional - for setting a specific folderid by default
[Parameter]
public string ShowFiles { get; set; } // optional - for indicating whether a list of files should be displayed - default is true
public bool ShowFiles { get; set; } = true; // optional - for indicating whether a list of files should be displayed - default is true
[Parameter]
public string FileId { get; set; } // optional - for setting a specific file by default
public bool ShowUpload { get; set; } = true; // optional - for indicating whether a Upload controls should be displayed - default is true
[Parameter]
public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true
[Parameter]
public int FileId { get; set; } = -1; // optional - for setting a specific file by default
[Parameter]
public string Filter { get; set; } // optional - comma delimited list of file types that can be selected or uploaded ie. "jpg,gif"
[Parameter]
public string UploadMultiple { get; set; } // optional - enable multiple file uploads - default false
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
protected override async Task OnInitializedAsync()
{
@ -129,56 +136,39 @@
if (!string.IsNullOrEmpty(Folder))
{
_folders = new List<Folder> {new Folder {FolderId = -1, Name = Folder}};
_folderid = -1;
FolderId = -1;
}
else
{
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
if (!string.IsNullOrEmpty(FolderId))
{
_folderid = int.Parse(FolderId);
}
}
if (!string.IsNullOrEmpty(FileId))
if (FileId != -1)
{
_fileid = int.Parse(FileId);
if (_fileid != -1)
File file = await FileService.GetFileAsync(FileId);
if (file != null)
{
File file = await FileService.GetFileAsync(int.Parse(FileId));
if (file != null)
{
_folderid = file.FolderId;
}
else
{
_fileid = -1; // file does not exist
}
FolderId = file.FolderId;
}
else
{
FileId = -1; // file does not exist
}
await SetImage();
}
if (!string.IsNullOrEmpty(ShowFiles))
{
_showfiles = bool.Parse(ShowFiles);
}
await SetImage();
if (!string.IsNullOrEmpty(Filter))
{
_filter = "." + Filter.Replace(",",",.");
_filter = "." + Filter.Replace(",", ",.");
}
await GetFiles();
// create unique id for component
// create unique id for component
_guid = Guid.NewGuid().ToString("N");
_fileinputid = _guid + "FileInput";
_progressinfoid = _guid + "ProgressInfo";
_progressbarid = _guid + "ProgressBar";
if (!string.IsNullOrEmpty(UploadMultiple))
{
_uploadmultiple = bool.Parse(UploadMultiple);
}
}
private async Task GetFiles()
@ -191,11 +181,11 @@
}
else
{
Folder folder = _folders.FirstOrDefault(item => item.FolderId == _folderid);
Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId);
if (folder != null)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User,PermissionNames.Edit, folder.Permissions);
_files = await FileService.GetFilesAsync(_folderid);
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.Permissions);
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
@ -222,9 +212,9 @@
_message = string.Empty;
try
{
_folderid = int.Parse((string)e.Value);
FolderId = int.Parse((string) e.Value);
await GetFiles();
_fileid = -1;
FileId = -1;
_image = string.Empty;
StateHasChanged();
}
@ -238,7 +228,7 @@
private async Task FileChanged(ChangeEventArgs e)
{
_message = string.Empty;
_fileid = int.Parse((string)e.Value);
FileId = int.Parse((string) e.Value);
await SetImage();
StateHasChanged();
@ -247,21 +237,21 @@
private async Task SetImage()
{
_image = string.Empty;
if (_fileid != -1)
if (FileId != -1)
{
File file = await FileService.GetFileAsync(_fileid);
File file = await FileService.GetFileAsync(FileId);
if (file != null && file.ImageHeight != 0 && file.ImageWidth != 0)
{
var maxwidth = 200;
var maxheight = 200;
var ratioX = (double)maxwidth / (double)file.ImageWidth;
var ratioY = (double)maxheight / (double)file.ImageHeight;
var ratioX = (double) maxwidth / (double) file.ImageWidth;
var ratioY = (double) maxheight / (double) file.ImageHeight;
var ratio = ratioX < ratioY ? ratioX : ratioY;
_image = "<img src=\"" + ContentUrl(_fileid) + "\" alt=\"" + file.Name +
"\" width=\"" + Convert.ToInt32(file.ImageWidth * ratio).ToString() +
"\" height=\"" + Convert.ToInt32(file.ImageHeight * ratio).ToString() + "\" />";
_image = "<img src=\"" + ContentUrl(FileId) + "\" alt=\"" + file.Name +
"\" width=\"" + Convert.ToInt32(file.ImageWidth * ratio).ToString() +
"\" height=\"" + Convert.ToInt32(file.ImageHeight * ratio).ToString() + "\" />";
}
}
}
@ -281,7 +271,7 @@
}
else
{
result = await FileService.UploadFilesAsync(_folderid, upload, _guid);
result = await FileService.UploadFilesAsync(FolderId, upload, _guid);
}
if (result == string.Empty)
@ -295,7 +285,7 @@
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault();
if (file != null)
{
_fileid = file.FileId;
FileId = file.FileId;
await SetImage();
}
}
@ -325,21 +315,21 @@
try
{
await FileService.DeleteFileAsync(_fileid);
await logger.LogInformation("File Deleted {File}", _fileid);
await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId);
_message = "<br /><div class=\"alert alert-success\" role=\"alert\">File Deleted</div>";
await GetFiles();
_fileid = -1;
FileId = -1;
await SetImage();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", _fileid, ex.Message);
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
_message = "<br /><div class=\"alert alert-danger\" role=\"alert\">Error Deleting File</div>";
}
}
public int GetFileId() => _fileid;
public int GetFileId() => FileId;
}

View File

@ -3,9 +3,8 @@
@using Oqtane.Modules.Controls
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject IHtmlTextService HtmlTextService
@inject NavigationManager NavigationManager
@inject HttpClient http
@inject SiteState sitestate
@if (_content != null)
{
@ -14,12 +13,28 @@
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
@if (!string.IsNullOrEmpty(_content))
{
<br /><br />
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
}
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Edit Html/Text";
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" },
// the following resources should be declared in the RichTextEditor component however the framework currently only supports resource management for modules and themes
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill1.3.6.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill1.3.6.snow.css" },
new Resource { ResourceType = ResourceType.Script, Url = "js/quill1.3.6.min.js" },
new Resource { ResourceType = ResourceType.Script, Url = "js/quill-blot-formatter.min.js" },
new Resource { ResourceType = ResourceType.Script, Url = "js/quill-interop.js" }
};
private RichTextEditor RichTextEditorHtml;
private string _content = null;
private string _createdby;
@ -27,16 +42,11 @@
private string _modifiedby;
private DateTime _modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Edit Html/Text";
protected override async Task OnInitializedAsync()
{
try
{
var htmltextservice = new HtmlTextService(http, sitestate);
var htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId);
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
_content = htmltext.Content;
@ -65,19 +75,18 @@
try
{
var htmltextservice = new HtmlTextService(http, sitestate);
var htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId);
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
htmltext.Content = content;
await htmltextservice.UpdateHtmlTextAsync(htmltext);
await HtmlTextService.UpdateHtmlTextAsync(htmltext);
}
else
{
htmltext = new HtmlTextInfo();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await htmltextservice.AddHtmlTextAsync(htmltext);
await HtmlTextService.AddHtmlTextAsync(htmltext);
}
await logger.LogInformation("Html/Text Content Saved {HtmlText}", htmltext);

View File

@ -1,32 +1,28 @@
@using Oqtane.Modules.HtmlText.Services
@using Oqtane.Modules.HtmlText.Models
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject HttpClient http
@inject SiteState sitestate
@inject IHtmlTextService HtmlTextService
@((MarkupString)content)
@if (PageState.EditMode)
{
<br />
}
<ActionLink Action="Edit" />
@if (PageState.EditMode)
{
<br /><br />
<br /><ActionLink Action="Edit" /><br /><br />
}
@code {
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
};
private string content = "";
protected override async Task OnParametersSetAsync()
{
try
{
var htmltextservice = new HtmlTextService(http, sitestate);
var htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId);
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
content = htmltext.Content;

View File

@ -8,7 +8,7 @@ using Oqtane.Shared;
namespace Oqtane.Modules.HtmlText.Services
{
public class HtmlTextService : ServiceBase, IHtmlTextService
public class HtmlTextService : ServiceBase, IHtmlTextService, IService
{
private readonly SiteState _siteState;

View File

@ -6,10 +6,11 @@ using Oqtane.Services;
using System;
using Oqtane.Enums;
using Oqtane.UI;
using System.Collections.Generic;
namespace Oqtane.Modules
{
public class ModuleBase : ComponentBase, IModuleControl
public abstract class ModuleBase : ComponentBase, IModuleControl
{
private Logger _logger;
@ -37,6 +38,9 @@ namespace Oqtane.Modules
public virtual bool UseAdminContainer { get { return true; } }
public virtual List<Resource> Resources { get; set; }
// path method
public string ModulePath()

View File

@ -6,7 +6,7 @@
<LangVersion>7.3</LangVersion>
<RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations>
<Version>0.9.1</Version>
<Version>1.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -27,10 +27,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0-rc1.20223.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.2" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0-rc1.20217.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.4" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -4,12 +4,16 @@ using System.Threading.Tasks;
using Oqtane.Services;
using System.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using Oqtane.Modules;
using Oqtane.Shared;
using Oqtane.Providers;
using Microsoft.AspNetCore.Components.Authorization;
using System.IO.Compression;
using System.IO;
namespace Oqtane.Client
{
@ -19,10 +23,9 @@ namespace Oqtane.Client
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
HttpClient httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
builder.Services.AddSingleton(
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }
);
builder.Services.AddSingleton(httpClient);
builder.Services.AddOptions();
// register auth services
@ -57,14 +60,16 @@ namespace Oqtane.Client
builder.Services.AddScoped<ISqlService, SqlService>();
builder.Services.AddScoped<ISystemService, SystemService>();
await LoadClientAssemblies(httpClient);
// dynamically register module contexts and repository services
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
Type[] implementationtypes = assembly.GetTypes()
.Where(item => item.GetInterfaces().Contains(typeof(IService)))
.ToArray();
foreach (Type implementationtype in implementationtypes)
var implementationTypes = assembly.GetTypes()
.Where(item => item.GetInterfaces().Contains(typeof(IService)));
foreach (Type implementationtype in implementationTypes)
{
Type servicetype = Type.GetType(implementationtype.AssemblyQualifiedName.Replace(implementationtype.Name, "I" + implementationtype.Name));
if (servicetype != null)
@ -76,9 +81,62 @@ namespace Oqtane.Client
builder.Services.AddScoped(implementationtype, implementationtype); // no interface defined for service
}
}
assembly.GetInstances<IClientStartup>()
.ToList()
.ForEach(x => x.ConfigureServices(builder.Services));
}
await builder.Build().RunAsync();
}
private static async Task LoadClientAssemblies(HttpClient http)
{
// get list of loaded assemblies on the client
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList();
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/~/api/Installation/load");
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
{
Dictionary<string, byte[]> dlls = new Dictionary<string, byte[]>();
Dictionary<string, byte[]> pdbs = new Dictionary<string, byte[]>();
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.Name)))
{
using (var memoryStream = new MemoryStream())
{
entry.Open().CopyTo(memoryStream);
byte[] file = memoryStream.ToArray();
switch (Path.GetExtension(entry.Name))
{
case ".dll":
dlls.Add(entry.Name, file);
break;
case ".pdb":
pdbs.Add(entry.Name, file);
break;
}
}
}
}
foreach (var item in dlls)
{
if (pdbs.ContainsKey(item.Key))
{
Assembly.Load(item.Value, pdbs[item.Key]);
}
else
{
Assembly.Load(item.Value);
}
}
}
}
}
}

View File

@ -5,16 +5,21 @@ using System.Linq;
using System.Collections.Generic;
using System.Net;
using System;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class AliasService : ServiceBase, IAliasService
{
public AliasService(HttpClient http) :base(http) { }
private string Apiurl => CreateApiUrl("Alias");
private readonly SiteState _siteState;
public AliasService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl(_siteState.Alias, "Alias");
public async Task<List<Alias>> GetAliasesAsync()
{

View File

@ -12,7 +12,6 @@ namespace Oqtane.Services
Task UpdateModuleDefinitionAsync(ModuleDefinition moduleDefinition);
Task InstallModuleDefinitionsAsync();
Task DeleteModuleDefinitionAsync(int moduleDefinitionId, int siteId);
Task LoadModuleDefinitionsAsync(int siteId, Runtime runtime);
Task CreateModuleDefinitionAsync(ModuleDefinition moduleDefinition, int moduleId);
}
}

View File

@ -3,14 +3,20 @@ using System.Threading.Tasks;
using System.Net.Http;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class JobLogService : ServiceBase, IJobLogService
{
public JobLogService(HttpClient http) :base(http) { }
private readonly SiteState _siteState;
private string Apiurl => CreateApiUrl("JobLog");
public JobLogService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl(_siteState.Alias, "JobLog");
public async Task<List<JobLog>> GetJobLogsAsync()
{

View File

@ -3,15 +3,21 @@ using System.Threading.Tasks;
using System.Net.Http;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class JobService : ServiceBase, IJobService
{
public JobService(HttpClient http) : base(http) { }
private readonly SiteState _siteState;
private string Apiurl => CreateApiUrl("Job");
public JobService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl(_siteState.Alias, "Job");
public async Task<List<Job>> GetJobsAsync()
{
List<Job> jobs = await GetJsonAsync<List<Job>>(Apiurl);

View File

@ -49,46 +49,9 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{moduleDefinitionId}?siteid={siteId}");
}
public async Task LoadModuleDefinitionsAsync(int siteId, Runtime runtime)
{
// get list of modules from the server
List<ModuleDefinition> moduledefinitions = await GetModuleDefinitionsAsync(siteId);
// download assemblies to browser when running client-side Blazor
if (runtime == Runtime.WebAssembly)
{
// get list of loaded assemblies on the client ( in the client-side hosting module the browser client has its own app domain )
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (ModuleDefinition moduledefinition in moduledefinitions)
{
// if a module has dependencies, check if they are loaded
if (moduledefinition.Dependencies != "")
{
foreach (string dependency in moduledefinition.Dependencies.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
string assemblyname = dependency.Replace(".dll", "");
if (assemblies.Where(item => item.FullName.StartsWith(assemblyname + ",")).FirstOrDefault() == null)
{
// download assembly from server and load
var bytes = await _http.GetByteArrayAsync($"{Apiurl}/load/{assemblyname}.dll");
Assembly.Load(bytes);
}
}
}
// check if the module assembly is loaded
if (assemblies.Where(item => item.FullName.StartsWith(moduledefinition.AssemblyName + ",")).FirstOrDefault() == null)
{
// download assembly from server and load
var bytes = await _http.GetByteArrayAsync($"{Apiurl}/load/{moduledefinition.AssemblyName}.dll");
Assembly.Load(bytes);
}
}
}
}
public async Task CreateModuleDefinitionAsync(ModuleDefinition moduleDefinition, int moduleId)
{
await PostJsonAsync($"{Apiurl}?moduleid={moduleId.ToString()}", moduleDefinition);
await PostJsonAsync($"{Apiurl}?moduleid={moduleId}", moduleDefinition);
}
}
}

View File

@ -135,13 +135,13 @@ namespace Oqtane.Services
//TODO Missing content JSON validation
}
// create an API Url which is tenant agnostic ( for use with entities in the MasterDB )
// create an API Url which is tenant agnostic ( for use during installation )
public string CreateApiUrl(string serviceName)
{
return CreateApiUrl(null, serviceName);
}
// create an API Url which is tenant aware ( for use with entities in the TenantDB )
// create an API Url which is tenant aware ( for use with repositories )
public string CreateApiUrl(Alias alias, string serviceName)
{
string apiurl = "/";

View File

@ -1,4 +1,5 @@
using Oqtane.Models;
using System;
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Linq;
@ -103,10 +104,10 @@ namespace Oqtane.Services
public async Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId)
{
var settingsList = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
foreach (KeyValuePair<string, string> kvp in settings)
{
Setting setting = settingsList.FirstOrDefault(item => item.SettingName == kvp.Key);
Setting setting = settingsList.FirstOrDefault(item => item.SettingName.Equals(kvp.Key,StringComparison.OrdinalIgnoreCase));
if (setting == null)
{
setting = new Setting();

View File

@ -1,4 +1,5 @@
using Oqtane.Models;
using Oqtane.Shared;
using System.Net.Http;
using System.Threading.Tasks;
@ -6,9 +7,14 @@ namespace Oqtane.Services
{
public class SqlService : ServiceBase, ISqlService
{
public SqlService(HttpClient http) : base(http) { }
private readonly SiteState _siteState;
private string Apiurl => CreateApiUrl("Sql");
public SqlService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl(_siteState.Alias, "Sql");
public async Task<SqlQuery> ExecuteQueryAsync(SqlQuery sqlquery)
{

View File

@ -3,14 +3,20 @@ using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class TenantService : ServiceBase, ITenantService
{
public TenantService(HttpClient http) : base(http) { }
private readonly SiteState _siteState;
private string Apiurl => CreateApiUrl("Tenant");
public TenantService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl(_siteState.Alias, "Tenant");
public async Task<List<Tenant>> GetTenantsAsync()
{

View File

@ -23,33 +23,6 @@ namespace Oqtane.Services
public async Task<List<Theme>> GetThemesAsync()
{
List<Theme> themes = await GetJsonAsync<List<Theme>>(Apiurl);
// get list of loaded assemblies
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Theme theme in themes)
{
if (theme.Dependencies != "")
{
foreach (string dependency in theme.Dependencies.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
string assemblyname = dependency.Replace(".dll", "");
if (assemblies.Where(item => item.FullName.StartsWith(assemblyname + ",")).FirstOrDefault() == null)
{
// download assembly from server and load
var bytes = await _http.GetByteArrayAsync($"{Apiurl}/load/{assemblyname}.dll");
Assembly.Load(bytes);
}
}
}
if (assemblies.Where(item => item.FullName.StartsWith(theme.AssemblyName + ",")).FirstOrDefault() == null)
{
// download assembly from server and load
var bytes = await _http.GetByteArrayAsync($"{Apiurl}/load/{theme.AssemblyName}.dll");
Assembly.Load(bytes);
}
}
return themes.OrderBy(item => item.Name).ToList();
}

View File

@ -28,9 +28,8 @@
@code {
public override string Panes => "Content";
protected override async Task OnParametersSetAsync()
public override List<Resource> Resources => new List<Resource>()
{
await IncludeCSS("Theme.css");
}
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }
};
}

View File

@ -17,7 +17,6 @@ namespace Oqtane.Themes
[CascadingParameter]
protected Module ModuleState { get; set; }
public virtual string Name { get; set; }
public string ThemePath()
{

View File

@ -149,7 +149,7 @@
<label for="Pane" class="control-label">Pane: </label>
<select class="form-control" @bind="@_pane">
<option value="">&lt;Select Pane&gt;</option>
@foreach (string pane in PageState.Page.Panes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
@foreach (string pane in PageState.Page.Panes)
{
<option value="@pane">@pane Pane</option>
}
@ -275,7 +275,7 @@
}
}
var panes = PageState.Page.Panes.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries);
var panes = PageState.Page.Panes;
_pane = panes.Count() == 1 ? panes.SingleOrDefault() : "";
var themes = await ThemeService.GetThemesAsync();
_containers = ThemeService.GetContainerTypes(themes);

View File

@ -60,7 +60,7 @@ namespace Oqtane.Themes.Controls
actionList.Add(new ActionViewModel {Name = "Move To Bottom", Action = async (s, m) => await MoveBottom(s, m)});
}
foreach (string pane in PageState.Page.Panes.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries))
foreach (string pane in PageState.Page.Panes)
{
if (pane != ModuleState.Pane)
{

View File

@ -2,6 +2,5 @@
{
public interface IContainerControl
{
string Name { get; }
}
}

View File

@ -19,11 +19,12 @@
@code {
public override string Panes => string.Empty;
protected override async Task OnParametersSetAsync()
public override List<Resource> Resources => new List<Resource>()
{
// go to https://www.bootstrapcdn.com/bootswatch/ and take your favorite theme
//<link href="https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css" rel="stylesheet" integrity="sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM" crossorigin="anonymous">
await LoadBootstrapTheme("https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css","sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM");
await IncludeCSS("Theme.css");
}
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "BootswatchCyborg.css" },
// remote stylesheets can be linked using the format below, however we want the default theme to display properly in local development scenarios where an Internet connection is not available
//new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css", Integrity = "sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM", CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }
};
}

View File

@ -0,0 +1,9 @@
@namespace Oqtane.Themes.OqtaneTheme
@inherits ContainerBase
<div class="container">
@if (PageState.EditMode)
{
<ModuleActions />
}
<ModuleInstance />
</div>

View File

@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Models;
using Oqtane.Shared;
using Oqtane.UI;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Oqtane.Themes
@ -11,31 +13,21 @@ namespace Oqtane.Themes
[Inject]
protected IJSRuntime JSRuntime { get; set; }
// optional interface properties
[CascadingParameter]
protected PageState PageState { get; set; }
public virtual string Panes { get; set; }
public virtual List<Resource> Resources { get; set; }
// path method
public string ThemePath()
{
return "Themes/" + GetType().Namespace + "/";
}
public async Task IncludeCSS(string Url)
{
if (!Url.StartsWith("http"))
{
Url = ThemePath() + Url;
}
var interop = new Interop(JSRuntime);
await interop.IncludeCSS("Theme", Url);
}
public async Task LoadBootstrapTheme(string url, string integrity = null)
{
var interop = new Interop(JSRuntime);
string crossorigin = string.IsNullOrEmpty(integrity) ? string.Empty : "anonymous";
await interop.IncludeLink("bootstrap", "stylesheet", url, "text/css", integrity, crossorigin);
}
// url methods
public string NavigateUrl()
{

View File

@ -7,7 +7,8 @@
<div class="container">
<div class="row">
<div class="mx-auto text-center">
<img src="oqtane.png" />
<img src="oqtane-black.png" />
<div style="font-weight: bold">Version: @Constants.Version</div>
</div>
</div>
<hr class="app-rule" />

View File

@ -118,6 +118,22 @@ namespace Oqtane.UI
}
}
public Task RemoveElementsById(string prefix, string first, string last)
{
try
{
_jsRuntime.InvokeAsync<string>(
"interop.removeElementsById",
prefix, first, last);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
public ValueTask<string> GetElementByName(string name)
{
try

View File

@ -32,7 +32,7 @@
}
else
{
_paneadminborder = "";
_paneadminborder = "container";
_panetitle = "";
}

View File

@ -11,7 +11,6 @@
@inject IPageService PageService
@inject IUserService UserService
@inject IModuleService ModuleService
@inject IModuleDefinitionService ModuleDefinitionService
@inject ILogService LogService
@implements IHandleAfterRender
@ -157,7 +156,6 @@
if (PageState == null || reload >= Reload.Site)
{
await ModuleDefinitionService.LoadModuleDefinitionsAsync(site.SiteId, runtime);
pages = await PageService.GetPagesAsync(site.SiteId);
}
else
@ -181,7 +179,7 @@
// extract admin route elements from path
var segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
int result;
// check if path has moduleid and control specification ie. page/moduleid/control/
// check if path has moduleid and action specification ie. pagename/moduleid/action/
if (segments.Length >= 2 && int.TryParse(segments[segments.Length - 2], out result))
{
action = segments[segments.Length - 1];
@ -190,7 +188,7 @@
}
else
{
// check if path has only moduleid specification ie. page/moduleid/
// check if path has moduleid specification ie. pagename/moduleid/
if (segments.Length >= 1 && int.TryParse(segments[segments.Length - 1], out result))
{
moduleid = result;
@ -240,21 +238,7 @@
{
page = await ProcessPage(page, site, user);
_pagestate = new PageState
{
Alias = alias,
Site = site,
Pages = pages,
Page = page,
User = user,
Uri = new Uri(_absoluteUri, UriKind.Absolute),
QueryString = querystring,
ModuleId = moduleid,
Action = action,
Runtime = runtime
};
if (PageState != null && (PageState.ModuleId != _pagestate.ModuleId || PageState.Action != _pagestate.Action))
if (PageState != null && (PageState.ModuleId != moduleid || PageState.Action != action))
{
reload = Reload.Page;
}
@ -262,15 +246,29 @@
if (PageState == null || reload >= Reload.Page)
{
modules = await ModuleService.GetModulesAsync(site.SiteId);
modules = ProcessModules(modules, page.PageId, _pagestate.ModuleId, _pagestate.Action, page.Panes, site.DefaultContainerType);
(page, modules) = ProcessModules(page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType);
}
else
{
modules = PageState.Modules;
}
_pagestate.Modules = modules;
_pagestate.EditMode = editmode;
_pagestate.LastSyncDate = lastsyncdate;
_pagestate = new PageState
{
Alias = alias,
Site = site,
Pages = pages,
Page = page,
User = user,
Modules = modules,
Uri = new Uri(_absoluteUri, UriKind.Absolute),
QueryString = querystring,
ModuleId = moduleid,
Action = action,
EditMode = editmode,
LastSyncDate = lastsyncdate,
Runtime = runtime
};
OnStateChange?.Invoke(_pagestate);
}
@ -357,19 +355,30 @@
page.ThemeType = site.DefaultThemeType;
page.LayoutType = site.DefaultLayoutType;
}
Type type;
page.Panes = new List<string>();
page.Resources = new List<Resource>();
string panes = "";
Type themetype = Type.GetType(page.ThemeType);
var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
if (themeobject != null)
{
panes = themeobject.Panes;
page.Resources = ManagePageResources(page.Resources, themeobject.Resources);
}
if (!string.IsNullOrEmpty(page.LayoutType))
{
type = Type.GetType(page.LayoutType);
}
else
{
type = Type.GetType(page.ThemeType);
Type layouttype = Type.GetType(page.LayoutType);
var layoutobject = Activator.CreateInstance(layouttype) as ILayoutControl;
if (layoutobject != null)
{
panes = layoutobject.Panes;
}
}
var property = type.GetProperty("Panes");
page.Panes = (string)property.GetValue(Activator.CreateInstance(type), null);
page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
catch
{
@ -379,12 +388,12 @@
return page;
}
private List<Module> ProcessModules(List<Module> modules, int pageid, int moduleid, string control, string panes, string defaultcontainertype)
private (Page Page, List<Module> Modules) ProcessModules(Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype)
{
var paneindex = new Dictionary<string, int>();
foreach (Module module in modules)
{
if (module.PageId == pageid || module.ModuleId == moduleid)
if (module.PageId == page.PageId || module.ModuleId == moduleid)
{
var typename = string.Empty;
if (module.ModuleDefinition != null)
@ -396,63 +405,72 @@
typename = Constants.ErrorModule;
}
if (module.ModuleId == moduleid && control != "")
if (module.ModuleId == moduleid && action != "")
{
// check if the module defines custom routes
if (module.ModuleDefinition.ControlTypeRoutes != "")
{
foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
if (route.StartsWith(control + "="))
if (route.StartsWith(action + "="))
{
typename = route.Replace(control + "=", "");
typename = route.Replace(action + "=", "");
}
}
}
module.ModuleType = typename.Replace(Constants.ActionToken, control);
// admin controls need to load additional metadata from the IModuleControl interface
if (moduleid == module.ModuleId)
{
typename = module.ModuleType;
// check for core module actions component
if (Constants.DefaultModuleActions.Contains(control))
{
typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, control);
}
Type moduletype = Type.GetType(typename);
if (moduletype != null)
{
var moduleobject = Activator.CreateInstance(moduletype);
module.SecurityAccessLevel = (SecurityAccessLevel)moduletype.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
module.ControlTitle = (string)moduletype.GetProperty("Title").GetValue(moduleobject);
module.Actions = (string)moduletype.GetProperty("Actions").GetValue(moduleobject);
module.UseAdminContainer = (bool)moduletype.GetProperty("UseAdminContainer").GetValue(moduleobject);
}
}
module.ModuleType = typename.Replace(Constants.ActionToken, action);
}
else
{
module.ModuleType = typename.Replace(Constants.ActionToken, Constants.DefaultAction);
}
// get additional metadata from IModuleControl interface
typename = module.ModuleType;
if (Constants.DefaultModuleActions.Contains(action))
{
// core framework module action components
typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action);
}
Type moduletype = Type.GetType(typename);
// ensure component implements IModuleControl
if (moduletype != null && !moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
{
module.ModuleType = "";
}
if (moduletype != null && module.ModuleType != "")
{
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources);
// additional metadata needed for admin components
if (module.ModuleId == moduleid && action != "")
{
module.SecurityAccessLevel = moduleobject.SecurityAccessLevel;
module.ControlTitle = moduleobject.Title;
module.Actions = moduleobject.Actions;
module.UseAdminContainer = moduleobject.UseAdminContainer;
}
}
// ensure module's pane exists in current page and if not, assign it to the Admin pane
if (panes == null || !panes.ToLower().Contains(module.Pane.ToLower()))
if (page.Panes == null || page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1)
{
module.Pane = Constants.AdminPane;
}
// calculate module position within pane
if (paneindex.ContainsKey(module.Pane))
if (paneindex.ContainsKey(module.Pane.ToLower()))
{
paneindex[module.Pane] += 1;
paneindex[module.Pane.ToLower()] += 1;
}
else
{
paneindex.Add(module.Pane, 0);
paneindex.Add(module.Pane.ToLower(), 0);
}
module.PaneModuleIndex = paneindex[module.Pane];
module.PaneModuleIndex = paneindex[module.Pane.ToLower()];
if (string.IsNullOrEmpty(module.ContainerType))
{
@ -461,16 +479,32 @@
}
}
foreach (Module module in modules.Where(item => item.PageId == pageid))
foreach (Module module in modules.Where(item => item.PageId == page.PageId))
{
module.PaneModuleCount = paneindex[module.Pane] + 1;
module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1;
}
return modules;
return (page, modules);
}
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources)
{
if (resources != null)
{
foreach (var resource in resources)
{
// ensure resource does not exist already
if (pageresources.Find(item => item.Url == resource.Url) == null)
{
pageresources.Add(resource);
}
}
}
return pageresources;
}
private Runtime GetRuntime()
=> RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))
? Runtime.WebAssembly
: Runtime.Server;
=> RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))
? Runtime.WebAssembly
: Runtime.Server;
}

View File

@ -12,6 +12,8 @@
protected override async Task OnParametersSetAsync()
{
var interop = new Interop(JsRuntime);
// set page title
if (!string.IsNullOrEmpty(PageState.Page.Title))
{
await interop.UpdateTitle(PageState.Page.Title);
@ -20,10 +22,34 @@
{
await interop.UpdateTitle(PageState.Site.Name + " - " + PageState.Page.Name);
}
// update page resources
int stylesheet = 0;
int script = 0;
foreach (Resource resource in PageState.Page.Resources)
{
switch (resource.ResourceType)
{
case ResourceType.Stylesheet:
stylesheet += 1;
await interop.IncludeLink("app-stylesheet" + stylesheet.ToString("00"), "stylesheet", resource.Url, "text/css", resource.Integrity ?? "", resource.CrossOrigin ?? "");
break;
case ResourceType.Script:
script += 1;
await interop.IncludeScript("app-script" + script.ToString("00"), resource.Url, "", "body", resource.Integrity ?? "", resource.CrossOrigin ?? "");
break;
}
}
// remove any page resources references which are no longer required for this page
await interop.RemoveElementsById("app-stylesheet", "app-stylesheet" + (stylesheet + 1).ToString("00"), "");
await interop.RemoveElementsById("app-script", "app-script" + (script + 1).ToString("00"), "");
// add favicon
if (PageState.Site.FaviconFileId != null)
{
await interop.IncludeLink("fav-icon", "shortcut icon", Utilities.ContentUrl(PageState.Alias, PageState.Site.FaviconFileId.Value), "image/x-icon", "", "");
}
// add PWA support
if (PageState.Site.PwaIsEnabled)
{
await InitializePwa(interop);

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>0.9.1</version>
<version>1.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -16,6 +16,7 @@ using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Microsoft.AspNetCore.Routing.Constraints;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@ -64,7 +65,7 @@ namespace Oqtane.Controllers
{
foreach (string file in Directory.GetFiles(folder))
{
files.Add(new Models.File {Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "")});
files.Add(new Models.File { Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "") });
}
}
}
@ -188,41 +189,54 @@ namespace Oqtane.Controllers
{
Models.File file = null;
Folder folder = _folders.GetFolder(int.Parse(folderid));
if (folder != null && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.Permissions))
{
string folderPath = GetFolderPath(folder);
CreateDirectory(folderPath);
string filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
// check for allowable file extensions
if (Constants.UploadableFiles.Contains(Path.GetExtension(filename).Replace(".", "")))
{
try
{
var client = new WebClient();
string targetPath = Path.Combine(folderPath, filename);
// remove file if it already exists
if (System.IO.File.Exists(targetPath))
{
System.IO.File.Delete(targetPath);
}
client.DownloadFile(url, targetPath);
_files.AddFile(CreateFile(filename, folder.FolderId, targetPath));
}
catch
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url {Url}", url);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
}
}
else
if (folder == null || !_userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.Permissions))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Download File {Url} {FolderId}", url, folderid);
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"User Not Authorized To Download File {Url} {FolderId}", url, folderid);
HttpContext.Response.StatusCode = 401;
return file;
}
string folderPath = GetFolderPath(folder);
CreateDirectory(folderPath);
string filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
// check for allowable file extensions
if (!Constants.UploadableFiles.Split(',')
.Contains(Path.GetExtension(filename).ToLower().Replace(".", "")))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return file;
}
if (!filename.IsPathOrFileValid())
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
$"File Could Not Be Downloaded From Url Due To Its File Name Not Allowed {url}");
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return file;
}
try
{
var client = new WebClient();
string targetPath = Path.Combine(folderPath, filename);
// remove file if it already exists
if (System.IO.File.Exists(targetPath))
{
System.IO.File.Delete(targetPath);
}
client.DownloadFile(url, targetPath);
file = _files.AddFile(CreateFile(filename, folder.FolderId, targetPath));
}
catch
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"File Could Not Be Downloaded From Url {Url}", url);
}
return file;
@ -232,46 +246,56 @@ namespace Oqtane.Controllers
[HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile file)
{
if (file.Length > 0)
if (file.Length <= 0)
{
string folderPath = "";
return;
}
if (int.TryParse(folder, out int folderId))
if (!file.FileName.IsPathOrFileValid())
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return;
}
string folderPath = "";
if (int.TryParse(folder, out int folderId))
{
Folder virtualFolder = _folders.GetFolder(folderId);
if (virtualFolder != null &&
_userPermissions.IsAuthorized(User, PermissionNames.Edit, virtualFolder.Permissions))
{
Folder virtualFolder = _folders.GetFolder(folderId);
if (virtualFolder != null && _userPermissions.IsAuthorized(User, PermissionNames.Edit, virtualFolder.Permissions))
{
folderPath = GetFolderPath(virtualFolder);
}
folderPath = GetFolderPath(virtualFolder);
}
else
}
else
{
if (User.IsInRole(Constants.HostRole))
{
if (User.IsInRole(Constants.HostRole))
{
folderPath = GetFolderPath(folder);
}
folderPath = GetFolderPath(folder);
}
}
if (folderPath != "")
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, file.FileName), FileMode.Create))
{
await file.CopyToAsync(stream);
}
if (folderPath != "")
string upload = await MergeFile(folderPath, file.FileName);
if (upload != "" && folderId != -1)
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, file.FileName), FileMode.Create))
{
await file.CopyToAsync(stream);
}
string upload = await MergeFile(folderPath, file.FileName);
if (upload != "" && folderId != -1)
{
_files.AddFile(CreateFile(upload, folderId, Path.Combine(folderPath, upload)));
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Upload File {Folder} {File}", folder, file);
HttpContext.Response.StatusCode = 401;
_files.AddFile(CreateFile(upload, folderId, Path.Combine(folderPath, upload)));
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"User Not Authorized To Upload File {Folder} {File}", folder, file);
HttpContext.Response.StatusCode = 401;
}
}
private async Task<string> MergeFile(string folder, string filename)
@ -282,7 +306,8 @@ namespace Oqtane.Controllers
string token = ".part_";
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "x_y"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = filename?.Substring(0, filename.IndexOf(token)); // base filename
filename = Path.GetFileNameWithoutExtension(filename); // base filename
string[] fileParts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist ( note that file parts can arrive out of order )
@ -317,7 +342,7 @@ namespace Oqtane.Controllers
}
// check for allowable file extensions
if (!Constants.UploadableFiles.Contains(Path.GetExtension(filename)?.Replace(".", "")))
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(filename)?.ToLower().Replace(".", "")))
{
System.IO.File.Delete(Path.Combine(folder, filename + ".tmp"));
}
@ -339,13 +364,15 @@ namespace Oqtane.Controllers
}
// clean up file parts which are more than 2 hours old ( which can happen if a prior file upload failed )
fileParts = Directory.GetFiles(folder, "*" + token + "*");
foreach (string filepart in fileParts)
var cleanupFiles = Directory.EnumerateFiles(folder, "*" + token + "*")
.Where(f => Path.GetExtension(f).StartsWith(token));
foreach (var file in cleanupFiles)
{
DateTime createddate = System.IO.File.GetCreationTime(filepart).ToUniversalTime();
if (createddate < DateTime.UtcNow.AddHours(-2))
var createdDate = System.IO.File.GetCreationTime(file).ToUniversalTime();
if (createdDate < DateTime.UtcNow.AddHours(-2))
{
System.IO.File.Delete(filepart);
System.IO.File.Delete(file);
}
}
@ -396,12 +423,13 @@ namespace Oqtane.Controllers
[HttpGet("download/{id}")]
public IActionResult Download(int id)
{
string errorpath = Path.Combine(GetFolderPath("images"), "error.png");
Models.File file = _files.GetFile(id);
if (file != null)
{
if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions))
{
string filepath = Path.Combine(GetFolderPath(file.Folder) , file.Name);
string filepath = Path.Combine(GetFolderPath(file.Folder), file.Name);
if (System.IO.File.Exists(filepath))
{
byte[] filebytes = System.IO.File.ReadAllBytes(filepath);
@ -411,22 +439,29 @@ namespace Oqtane.Controllers
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FileId} {FilePath}", id, filepath);
HttpContext.Response.StatusCode = 404;
return null;
if (System.IO.File.Exists(errorpath))
{
byte[] filebytes = System.IO.File.ReadAllBytes(errorpath);
return File(filebytes, "application/octet-stream", file.Name);
}
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access File {FileId}", id);
HttpContext.Response.StatusCode = 401;
return null;
byte[] filebytes = System.IO.File.ReadAllBytes(errorpath);
return File(filebytes, "application/octet-stream", file.Name);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Not Found {FileId}", id);
HttpContext.Response.StatusCode = 404;
return null;
byte[] filebytes = System.IO.File.ReadAllBytes(errorpath);
return File(filebytes, "application/octet-stream", "error.png");
}
return null;
}
private string GetFolderPath(Folder folder)
@ -448,7 +483,7 @@ namespace Oqtane.Controllers
string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries);
foreach (string folder in folders)
{
path = Utilities.PathCombine(path, folder,"\\");
path = Utilities.PathCombine(path, folder, "\\");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
@ -465,11 +500,11 @@ namespace Oqtane.Controllers
FileInfo fileinfo = new FileInfo(filepath);
file.Extension = fileinfo.Extension.ToLower().Replace(".", "");
file.Size = (int) fileinfo.Length;
file.Size = (int)fileinfo.Length;
file.ImageHeight = 0;
file.ImageWidth = 0;
if (Constants.ImageFiles.Contains(file.Extension))
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
{
FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
using (var image = Image.FromStream(stream))

View File

@ -10,7 +10,6 @@ using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using System.IO;
namespace Oqtane.Controllers
{
@ -33,7 +32,7 @@ namespace Oqtane.Controllers
public IEnumerable<Folder> Get(string siteid)
{
List<Folder> folders = new List<Folder>();
foreach(Folder folder in _folders.GetFolders(int.Parse(siteid)))
foreach (Folder folder in _folders.GetFolders(int.Parse(siteid)))
{
if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.Permissions))
{
@ -85,7 +84,7 @@ namespace Oqtane.Controllers
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = Constants.RegisteredRole)]
@ -104,15 +103,25 @@ namespace Oqtane.Controllers
new Permission(PermissionNames.Edit, Constants.AdminRole, true),
}.EncodePermissions();
}
if (_userPermissions.IsAuthorized(User,PermissionNames.Edit, permissions))
if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, permissions))
{
if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null)
if (folder.IsPathValid())
{
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.PathCombine(parent.Path, folder.Name,"\\");
if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null)
{
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.PathCombine(parent.Path, folder.Name);
}
folder.Path = Utilities.PathCombine(folder.Path, "\\");
folder = _folders.AddFolder(folder);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Name Not Valid {Folder}", folder);
HttpContext.Response.StatusCode = 401;
folder = null;
}
folder = _folders.AddFolder(folder);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder);
}
else
{
@ -131,13 +140,23 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid && _userPermissions.IsAuthorized(User, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
{
if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null)
if (folder.IsPathValid())
{
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.PathCombine(parent.Path, folder.Name,"\\");
if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null)
{
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.PathCombine(parent.Path, folder.Name);
}
folder.Path = Utilities.PathCombine(folder.Path, "\\");
folder = _folders.UpdateFolder(folder);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Folder Updated {Folder}", folder);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Name Not Valid {Folder}", folder);
HttpContext.Response.StatusCode = 401;
folder = null;
}
folder = _folders.UpdateFolder(folder);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Folder Updated {Folder}", folder);
}
else
{

View File

@ -4,6 +4,11 @@ using Microsoft.Extensions.Configuration;
using Oqtane.Models;
using Oqtane.Shared;
using Oqtane.Infrastructure;
using System;
using System.IO;
using System.Reflection;
using System.Linq;
using System.IO.Compression;
namespace Oqtane.Controllers
{
@ -55,5 +60,56 @@ namespace Oqtane.Controllers
_installationManager.UpgradeFramework();
return installation;
}
// GET api/<controller>/load
[HttpGet("load")]
public IActionResult Load()
{
if (_config.GetSection("Runtime").Value == "WebAssembly")
{
// get list of assemblies which should be downloaded to browser
var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies();
var list = assemblies.Select(a => a.GetName().Name).ToList();
var deps = assemblies.SelectMany(a => a.GetReferencedAssemblies()).Distinct();
list.AddRange(deps.Where(a => a.Name.EndsWith(".oqtane", StringComparison.OrdinalIgnoreCase)).Select(a => a.Name));
// create zip file containing assemblies and debug symbols
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
byte[] zipfile;
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
ZipArchiveEntry entry;
foreach (string file in list)
{
entry = archive.CreateEntry(file + ".dll");
using (var filestream = new FileStream(Path.Combine(binfolder, file + ".dll"), FileMode.Open, FileAccess.Read))
using (var entrystream = entry.Open())
{
filestream.CopyTo(entrystream);
}
if (System.IO.File.Exists(Path.Combine(binfolder, file + ".pdb")))
{
entry = archive.CreateEntry(file + ".pdb");
using (var filestream = new FileStream(Path.Combine(binfolder, file + ".pdb"), FileMode.Open, FileAccess.Read))
using (var entrystream = entry.Open())
{
filestream.CopyTo(entrystream);
}
}
}
}
zipfile = memoryStream.ToArray();
}
return File(zipfile, "application/octet-stream", "oqtane.zip");
}
else
{
HttpContext.Response.StatusCode = 401;
return null;
}
}
}
}

View File

@ -13,7 +13,8 @@ using Oqtane.Repository;
using Oqtane.Security;
using System;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable StringIndexOfIsCultureSpecific.1
using Microsoft.Extensions.Configuration;
using System.Xml.Linq;
namespace Oqtane.Controllers
{
@ -23,20 +24,24 @@ namespace Oqtane.Controllers
private readonly IModuleDefinitionRepository _moduleDefinitions;
private readonly IModuleRepository _modules;
private readonly ITenantRepository _tenants;
private readonly ISqlRepository _sql;
private readonly IUserPermissions _userPermissions;
private readonly IInstallationManager _installationManager;
private readonly IWebHostEnvironment _environment;
private readonly IConfigurationRoot _config;
private readonly IServiceProvider _serviceProvider;
private readonly ILogManager _logger;
public ModuleDefinitionController(IModuleDefinitionRepository moduleDefinitions, IModuleRepository modules,ITenantRepository tenants, IUserPermissions userPermissions, IInstallationManager installationManager, IWebHostEnvironment environment, IServiceProvider serviceProvider, ILogManager logger)
public ModuleDefinitionController(IModuleDefinitionRepository moduleDefinitions, IModuleRepository modules,ITenantRepository tenants, ISqlRepository sql, IUserPermissions userPermissions, IInstallationManager installationManager, IWebHostEnvironment environment, IConfigurationRoot config, IServiceProvider serviceProvider, ILogManager logger)
{
_moduleDefinitions = moduleDefinitions;
_modules = modules;
_tenants = tenants;
_sql = sql;
_userPermissions = userPermissions;
_installationManager = installationManager;
_environment = environment;
_config = config;
_serviceProvider = serviceProvider;
_logger = logger;
}
@ -99,75 +104,60 @@ namespace Oqtane.Controllers
public void Delete(int id, int siteid)
{
ModuleDefinition moduledefinition = _moduleDefinitions.GetModuleDefinition(id, siteid);
if (moduledefinition != null)
if (moduledefinition != null )
{
if (!string.IsNullOrEmpty(moduledefinition.ServerManagerType))
if (!string.IsNullOrEmpty(moduledefinition.ServerManagerType) && Utilities.GetAssemblyName(moduledefinition.ServerManagerType) != "Oqtane.Server")
{
Type moduletype = Type.GetType(moduledefinition.ServerManagerType);
if (moduletype != null && moduletype.GetInterface("IInstallable") != null)
foreach (Tenant tenant in _tenants.GetTenants())
{
foreach (Tenant tenant in _tenants.GetTenants())
try
{
try
if (moduletype.GetInterface("IInstallable") != null)
{
var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype);
((IInstallable)moduleobject).Uninstall(tenant);
}
catch
else
{
// an error occurred executing the uninstall
_sql.ExecuteScript(tenant, moduletype.Assembly, Utilities.GetTypeName(moduledefinition.ModuleDefinitionName) + ".Uninstall.sql");
}
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "{ModuleDefinitionName} Uninstalled For Tenant {Tenant}", moduledefinition.ModuleDefinitionName, tenant.Name);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Error Uninstalling {ModuleDefinitionName} For Tenant {Tenant} {Error}", moduledefinition.ModuleDefinitionName, tenant.Name, ex.Message);
}
}
}
// format root assembly name
string assemblyname = Utilities.GetAssemblyName(moduledefinition.ModuleDefinitionName);
if (assemblyname != "Oqtane.Client")
{
assemblyname = assemblyname.Replace(".Client", "");
// clean up module static resource folder
string folder = Path.Combine(_environment.WebRootPath, Path.Combine("Modules",assemblyname));
string folder = Path.Combine(_environment.WebRootPath, Path.Combine("Modules", Utilities.GetTypeName(moduledefinition.ModuleDefinitionName)));
if (Directory.Exists(folder))
{
Directory.Delete(folder, true);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Static Resources Removed For {AssemblynName}", assemblyname);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Static Resources Removed For {ModuleDefinitionName}", moduledefinition.ModuleDefinitionName);
}
// remove module assembly from /bin
// get root assembly name ( note that this only works if modules follow a specific naming convention for their assemblies )
string assemblyname = Utilities.GetAssemblyName(moduledefinition.ModuleDefinitionName).ToLower();
assemblyname = assemblyname.Replace(".client", "").Replace(".oqtane", "");
// remove module assemblies from /bin
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
foreach (string file in Directory.EnumerateFiles(binfolder, assemblyname + "*.*"))
{
System.IO.File.Delete(file);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Assembly Removed {Filename}", file);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Assembly {Filename} Removed For {ModuleDefinitionName}", file, moduledefinition.ModuleDefinitionName);
}
// remove module definition
_moduleDefinitions.DeleteModuleDefinition(id, siteid);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Definition {ModuleDefinitionName} Deleted", moduledefinition.Name);
// restart application
_installationManager.RestartApplication();
}
// remove module definition
_moduleDefinitions.DeleteModuleDefinition(id, siteid);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Definition Deleted {ModuleDefinitionName}", moduledefinition.Name);
// restart application
_installationManager.RestartApplication();
}
}
// GET api/<controller>/load/assembyname
[HttpGet("load/{assemblyname}")]
public IActionResult Load(string assemblyname)
{
if (Path.GetExtension(assemblyname).ToLower() == ".dll")
{
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
byte[] file = System.IO.File.ReadAllBytes(Path.Combine(binfolder, assemblyname));
return File(file, "application/octet-stream", assemblyname);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Download Assembly {Assembly}", assemblyname);
HttpContext.Response.StatusCode = 401;
return null;
}
}
@ -185,13 +175,13 @@ namespace Oqtane.Controllers
if (moduleDefinition.Template == "internal")
{
rootPath = Utilities.PathCombine(rootFolder.FullName,"\\");
moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Modules, Oqtane.Client";
moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + "s, Oqtane.Client";
moduleDefinition.ServerManagerType = moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Manager." + moduleDefinition.Name + "Manager, Oqtane.Server";
}
else
{
rootPath = Utilities.PathCombine(rootFolder.Parent.FullName , moduleDefinition.Owner + "." + moduleDefinition.Name + "s","\\");
moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Modules, " + moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Client.Oqtane";
moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + "s, " + moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Client.Oqtane";
moduleDefinition.ServerManagerType = moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Manager." + moduleDefinition.Name + "Manager, " + moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Server.Oqtane";
}
@ -204,7 +194,11 @@ namespace Oqtane.Controllers
if (moduleDefinition.Template == "internal")
{
// need logic to add embedded scripts to Oqtane.Server.csproj - also you need to remove them on uninstall
// add embedded resources to project
List<string> resources = new List<string>();
resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name + "s", "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + "s.1.0.0.sql"));
resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name + "s", "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Uninstall.sql"));
EmbedResourceFiles(Utilities.PathCombine(rootPath, "Oqtane.Server", "Oqtane.Server.csproj"), resources);
}
_installationManager.RestartApplication();
@ -253,5 +247,19 @@ namespace Oqtane.Controllers
}
}
}
private void EmbedResourceFiles(string projectfile, List<string> resources)
{
XDocument project = XDocument.Load(projectfile);
var itemGroup = project.Descendants("ItemGroup").Descendants("EmbeddedResource").FirstOrDefault().Parent;
if (itemGroup != null)
{
foreach (var resource in resources)
{
itemGroup.Add(new XElement("EmbeddedResource", new XAttribute("Include", resource)));
}
}
project.Save(projectfile);
}
}
}

View File

@ -166,6 +166,7 @@ namespace Oqtane.Controllers
page.EditMode = false;
page.ThemeType = parent.ThemeType;
page.LayoutType = parent.LayoutType;
page.DefaultContainerType = parent.DefaultContainerType;
page.Icon = parent.Icon;
page.Permissions = new List<Permission> {
new Permission(PermissionNames.View, userid, true),

View File

@ -1,7 +1,9 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
@ -17,6 +19,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>
[HttpGet]
[Authorize(Roles = Constants.HostRole)]
public IEnumerable<SiteTemplate> Get()
{
return _siteTemplates.GetSiteTemplates();

View File

@ -54,22 +54,20 @@ namespace Oqtane.Controllers
{
List<Theme> themes = _themes.GetThemes().ToList();
Theme theme = themes.Where(item => item.ThemeName == themename).FirstOrDefault();
if (theme != null)
if (theme != null && Utilities.GetAssemblyName(theme.ThemeName) != "Oqtane.Client")
{
themename = theme.ThemeName.Substring(0, theme.ThemeName.IndexOf(","));
string folder = Path.Combine(_environment.WebRootPath, "Themes" , themename);
// clean up theme static resource folder
string folder = Path.Combine(_environment.WebRootPath, "Themes" , Utilities.GetTypeName(theme.ThemeName));
if (Directory.Exists(folder))
{
Directory.Delete(folder, true);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Theme Static Resources Removed For {ThemeName}", theme.ThemeName);
}
// remove theme assembly from /bin
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
foreach (string file in Directory.EnumerateFiles(binfolder, themename + "*.dll"))
{
System.IO.File.Delete(file);
}
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Theme Deleted {ThemeName}", themename);
System.IO.File.Delete(Path.Combine(binfolder, Utilities.GetAssemblyName(theme.ThemeName) + ".dll"));
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Theme Assembly {Filename} Removed For {ThemeName}", Utilities.GetAssemblyName(theme.ThemeName) + ".dll", themename);
_installationManager.RestartApplication();
}

View File

@ -0,0 +1,26 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Oqtane.Infrastructure;
namespace Oqtane.Extensions
{
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder ConfigureOqtaneAssemblies(this IApplicationBuilder app, IWebHostEnvironment env)
{
var startUps = AppDomain.CurrentDomain
.GetOqtaneAssemblies()
.SelectMany(x => x.GetInstances<IServerStartup>());
foreach (var startup in startUps)
{
startup.Configure(app, env);
}
return app;
}
}
}

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Oqtane.Infrastructure;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
@ -30,6 +31,22 @@ namespace Microsoft.Extensions.DependencyInjection
}
}
}
return mvcBuilder;
}
public static IMvcBuilder ConfigureOqtaneMvc(this IMvcBuilder mvcBuilder)
{
var startUps = AppDomain.CurrentDomain
.GetOqtaneAssemblies()
.SelectMany(x => x.GetInstances<IServerStartup>());
foreach (var startup in startUps)
{
startup.ConfigureMvc(mvcBuilder);
}
return mvcBuilder;
}
}

View File

@ -8,21 +8,23 @@ using Microsoft.Extensions.Hosting;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class OqtaneServiceCollectionExtensions
{
public static IServiceCollection AddOqtaneParts(this IServiceCollection services)
public static IServiceCollection AddOqtaneParts(this IServiceCollection services, Runtime runtime)
{
LoadAssemblies();
services.AddOqtaneServices();
services.AddOqtaneServices(runtime);
return services;
}
private static IServiceCollection AddOqtaneServices(this IServiceCollection services)
private static IServiceCollection AddOqtaneServices(this IServiceCollection services, Runtime runtime)
{
if (services is null)
{
@ -53,11 +55,24 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton(hostedServiceType, serviceType);
}
}
var startUps = assembly.GetInstances<IServerStartup>();
foreach (var startup in startUps)
{
startup.ConfigureServices(services);
}
if (runtime == Runtime.Server)
{
assembly.GetInstances<IClientStartup>()
.ToList()
.ForEach(x => x.ConfigureServices(services));
}
}
return services;
}
private static void LoadAssemblies()
{
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);

View File

@ -332,10 +332,13 @@ namespace Oqtane.Infrastructure
using (var scope = _serviceScopeFactory.CreateScope())
{
var moduledefinitions = scope.ServiceProvider.GetRequiredService<IModuleDefinitionRepository>();
var sql = scope.ServiceProvider.GetRequiredService<ISqlRepository>();
foreach (var moduledefinition in moduledefinitions.GetModuleDefinitions())
{
if (!string.IsNullOrEmpty(moduledefinition.ServerManagerType) && !string.IsNullOrEmpty(moduledefinition.ReleaseVersions))
if (!string.IsNullOrEmpty(moduledefinition.ReleaseVersions) && !string.IsNullOrEmpty(moduledefinition.ServerManagerType))
{
Type moduletype = Type.GetType(moduledefinition.ServerManagerType);
string[] versions = moduledefinition.ReleaseVersions.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
using (var db = new InstallationContext(NormalizeConnectionString(_config.GetConnectionString(SettingKeys.ConnectionStringKey))))
{
@ -351,19 +354,22 @@ namespace Oqtane.Infrastructure
if (index == -1) index = 0;
for (int i = index; i < versions.Length; i++)
{
Type moduletype = Type.GetType(moduledefinition.ServerManagerType);
if (moduletype != null && moduletype.GetInterface("IInstallable") != null)
try
{
try
if (moduletype.GetInterface("IInstallable") != null)
{
var moduleobject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduletype);
((IInstallable)moduleobject).Install(tenant, versions[i]);
((IInstallable)moduleobject).Install(tenant, versions[i]);
}
catch (Exception ex)
else
{
result.Message = "An Error Occurred Installing " + moduledefinition.Name + " - " + ex.Message.ToString();
sql.ExecuteScript(tenant, moduletype.Assembly, Utilities.GetTypeName(moduledefinition.ModuleDefinitionName) + "." + versions[i] + ".sql");
}
}
catch (Exception ex)
{
result.Message = "An Error Occurred Installing " + moduledefinition.Name + " Version " + versions[i] + " - " + ex.Message.ToString();
}
}
}
}

View File

@ -28,7 +28,7 @@ namespace Oqtane.Infrastructure
{
var webRootPath = _environment.WebRootPath;
var install = UnpackPackages(folders, webRootPath);
var install = InstallPackages(folders, webRootPath);
if (install && restart)
{
@ -36,7 +36,7 @@ namespace Oqtane.Infrastructure
}
}
public static bool UnpackPackages(string folders, string webRootPath)
public static bool InstallPackages(string folders, string webRootPath)
{
bool install = false;
string binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
@ -44,14 +44,12 @@ namespace Oqtane.Infrastructure
foreach (string folder in folders.Split(','))
{
string sourceFolder = Path.Combine(webRootPath, folder);
// create folder if it does not exist
if (!Directory.Exists(sourceFolder))
{
Directory.CreateDirectory(sourceFolder);
}
// iterate through packages
// iterate through Nuget packages in source folder
foreach (string packagename in Directory.GetFiles(sourceFolder, "*.nupkg"))
{
string name = Path.GetFileNameWithoutExtension(packagename);
@ -89,28 +87,25 @@ namespace Oqtane.Infrastructure
// deploy to appropriate locations
foreach (ZipArchiveEntry entry in archive.Entries)
{
string foldername = Path.GetDirectoryName(entry.FullName).Split('\\')[0];
string filename = Path.GetFileName(entry.FullName);
switch (Path.GetExtension(filename).ToLower())
{
case ".pdb":
case ".dll":
if (binFolder != null) entry.ExtractToFile(Path.Combine(binFolder, filename), true);
break;
case ".png":
case ".jpg":
case ".jpeg":
case ".gif":
case ".svg":
case ".js":
case ".css":
string entryPath = Utilities.PathCombine(entry.FullName.Replace("wwwroot", name).Split('/'));
filename = Path.Combine(sourceFolder, entryPath);
if (!Directory.Exists(Path.GetDirectoryName(filename)))
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
switch (foldername)
{
case "lib":
filename = Path.Combine(binFolder, filename);
ExtractFile(entry, filename);
break;
case "wwwroot":
filename = Path.Combine(sourceFolder, Utilities.PathCombine(entry.FullName.Replace("wwwroot", name).Split('/')));
ExtractFile(entry, filename);
break;
case "content":
if (Path.GetDirectoryName(entry.FullName) != "content") // assets must be in subfolders
{
filename = Path.Combine(webRootPath, Utilities.PathCombine(entry.FullName.Replace("content", "").Split('/')));
ExtractFile(entry, filename);
}
break;
}
}
@ -126,6 +121,15 @@ namespace Oqtane.Infrastructure
return install;
}
private static void ExtractFile(ZipArchiveEntry entry, string filename)
{
if (!Directory.Exists(Path.GetDirectoryName(filename)))
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
}
public void UpgradeFramework()
{
string folder = Path.Combine(_environment.WebRootPath, "Framework");

View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace Oqtane.Infrastructure
{
public interface IServerStartup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
void ConfigureServices(IServiceCollection services);
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
void Configure(IApplicationBuilder app, IWebHostEnvironment env);
void ConfigureMvc(IMvcBuilder mvcBuilder);
}
}

View File

@ -55,9 +55,9 @@ namespace Oqtane.SiteTemplates
new Permission(PermissionNames.View, Constants.AdminRole, true),
new Permission(PermissionNames.Edit, Constants.AdminRole, true)
}.EncodePermissions(),
Content = "<p><a href=\"https://www.oqtane.org\" target=\"_new\">Oqtane</a> is an open source <b>modular application framework</b> built from the ground up using modern .NET Core technology. It leverages the revolutionary new Blazor component model to create a <b>fully dynamic</b> web development experience which can be executed on a client or server. Whether you are looking for a platform to <b>accelerate your web development</b> efforts, or simply interested in exploring the anatomy of a large-scale Blazor application, Oqtane provides a solid foundation based on proven enterprise architectural principles.</p>" +
"<p align=\"center\"><a href=\"https://www.oqtane.org\" target=\"_new\"><img class=\"img-fluid\" src=\"oqtane.png\"></a></p><p align=\"center\"><a class=\"btn btn-primary\" href=\"https://www.oqtane.org/Community\" target=\"_new\">Join Our Community</a>&nbsp;&nbsp;<a class=\"btn btn-primary\" href=\"https://github.com/oqtane/oqtane.framework\" target=\"_new\">Clone Our Repo</a></p>" +
"<p><a href=\"https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor\" target=\"_new\">Blazor</a> is a single-page app framework that lets you build interactive web applications using C# instead of JavaScript. Client-side Blazor relies on WebAssembly, an open web standard that does not require plugins or code transpilation in order to run natively in a web browser. Server-side Blazor uses SignalR to host your application on a web server and provide a responsive and robust debugging experience. Blazor applications works in all modern web browsers, including mobile browsers.</p>" +
Content = "<p><a href=\"https://www.oqtane.org\" target=\"_new\">Oqtane</a> is an open source <b>modular application framework</b> that provides advanced functionality for developing web and mobile applications on ASP.NET Core. It leverages the revolutionary new Blazor component model to compose a <b>fully dynamic</b> web development experience which can be hosted either client-side or server-side. Whether you are looking for a platform to <b>accelerate your web development</b> efforts, or simply interested in exploring the anatomy of a large-scale Blazor application, Oqtane provides a solid foundation based on proven enterprise architectural principles.</p>" +
"<p align=\"center\"><a href=\"https://www.oqtane.org\" target=\"_new\"><img class=\"img-fluid\" src=\"oqtane-white.png\"></a></p><p align=\"center\"><a class=\"btn btn-primary\" href=\"https://www.oqtane.org/Community\" target=\"_new\">Join Our Community</a>&nbsp;&nbsp;<a class=\"btn btn-primary\" href=\"https://github.com/oqtane/oqtane.framework\" target=\"_new\">Clone Our Repo</a></p>" +
"<p><a href=\"https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor\" target=\"_new\">Blazor</a> is an open source and cross-platform web UI framework for building single-page apps using .NET and C# instead of JavaScript. Blazor WebAssembly relies on Wasm, an open web standard that does not require plugins or code transpilation in order to run natively in a web browser. Blazor Server uses SignalR to host your application on a web server and provide a responsive and robust development experience. Blazor applications work in all modern web browsers, including mobile browsers.</p>" +
"<p>Blazor is a feature of <a href=\"https://dotnet.microsoft.com/apps/aspnet\" target=\"_new\">ASP.NET Core 3</a>, the popular cross platform web development framework from Microsoft that extends the <a href=\"https://dotnet.microsoft.com/learn/dotnet/what-is-dotnet\" target=\"_new\" >.NET developer platform</a> with tools and libraries for building web apps.</p>"
},
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = "Content",
@ -132,16 +132,16 @@ namespace Oqtane.SiteTemplates
}
});
if (System.IO.File.Exists(Path.Combine(_environment.WebRootPath, "images", "logo.png")))
if (System.IO.File.Exists(Path.Combine(_environment.WebRootPath, "images", "logo-white.png")))
{
string folderpath = Utilities.PathCombine(_environment.ContentRootPath, "Content", "Tenants", site.TenantId.ToString(), "Sites", site.SiteId.ToString(),"\\");
System.IO.Directory.CreateDirectory(folderpath);
if (!System.IO.File.Exists(Path.Combine(folderpath, "logo.png")))
if (!System.IO.File.Exists(Path.Combine(folderpath, "logo-white.png")))
{
System.IO.File.Copy(Path.Combine(_environment.WebRootPath, "images", "logo.png"), Path.Combine(folderpath, "logo.png"));
System.IO.File.Copy(Path.Combine(_environment.WebRootPath, "images", "logo-white.png"), Path.Combine(folderpath, "logo-white.png"));
}
Folder folder = _folderRepository.GetFolder(site.SiteId, "");
Oqtane.Models.File file = _fileRepository.AddFile(new Oqtane.Models.File { FolderId = folder.FolderId, Name = "logo.png", Extension = "png", Size = 8192, ImageHeight = 80, ImageWidth = 250 });
Oqtane.Models.File file = _fileRepository.AddFile(new Oqtane.Models.File { FolderId = folder.FolderId, Name = "logo-white.png", Extension = "png", Size = 8192, ImageHeight = 80, ImageWidth = 250 });
site.LogoFileId = file.FileId;
_siteRepository.UpdateSite(site);
}

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>7.3</LangVersion>
<Configurations>Debug;Release</Configurations>
<Version>0.9.1</Version>
<Version>1.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -16,33 +16,26 @@
<PackageReleaseNotes>Not for production use.</PackageReleaseNotes>
<RootNamespace>Oqtane</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="Scripts\Tenant.0.9.1.sql" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Scripts\Master.0.9.0.sql" />
<EmbeddedResource Include="Scripts\Tenant.0.9.0.sql" />
<EmbeddedResource Include="Scripts\Tenant.0.9.1.sql" />
<EmbeddedResource Include="Scripts\Tenant.0.9.2.sql" />
<EmbeddedResource Include="Modules\HtmlText\Scripts\HtmlText.1.0.0.sql" />
<EmbeddedResource Include="Modules\HtmlText\Scripts\HtmlText.Uninstall.sql" />
<EmbeddedResource Include="Scripts\Master.0.9.0.sql" />
<EmbeddedResource Include="Scripts\Tenant.0.9.1.sql" />
<EmbeddedResource Include="Scripts\Tenant.0.9.0.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="dbup" Version="4.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -14,8 +14,6 @@
<link id="fav-icon" rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<!-- stub the PWA manifest but defer the assignment of href -->
<link id="pwa-manifest" rel="manifest" />
<link href="css/quill/quill1.3.6.bubble.css" rel="stylesheet" />
<link href="css/quill/quill1.3.6.snow.css" rel="stylesheet" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="css/app.css" rel="stylesheet" />
</head>
@ -25,10 +23,7 @@
<component type="typeof(Oqtane.App)" render-mode="Server" />
</app>
<script src="js/site.js"></script>
<script src="js/interop.js"></script>
<script src="js/quill1.3.6.min.js"></script>
<script src="js/quill-blot-formatter.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>

View File

@ -213,8 +213,8 @@ namespace Oqtane.Repository
if (moduletype != null)
{
// get property values from IModule
var moduleobject = Activator.CreateInstance(moduletype);
moduledefinition = (ModuleDefinition) moduletype.GetProperty("ModuleDefinition").GetValue(moduleobject);
var moduleobject = Activator.CreateInstance(moduletype) as IModule;
moduledefinition = moduleobject.ModuleDefinition;
}
else
{
@ -261,8 +261,8 @@ namespace Oqtane.Repository
moduledefinition = moduledefinitions[index];
// actions
var modulecontrolobject = Activator.CreateInstance(modulecontroltype);
string actions = (string) modulecontroltype.GetProperty("Actions")?.GetValue(modulecontrolobject);
var modulecontrolobject = Activator.CreateInstance(modulecontroltype) as IModuleControl;
string actions = modulecontrolobject.Actions;
if (!string.IsNullOrEmpty(actions))
{
foreach (string action in actions.Split(','))

View File

@ -757,6 +757,7 @@ namespace Oqtane.Repository
EditMode = pagetemplate.EditMode,
ThemeType = "",
LayoutType = "",
DefaultContainerType = "",
Icon = pagetemplate.Icon,
Permissions = pagetemplate.PagePermissions,
IsPersonalizable = pagetemplate.IsPersonalizable,

View File

@ -66,8 +66,8 @@ namespace Oqtane.Repository
.Where(item => item.GetInterfaces().Contains(typeof(ITheme))).FirstOrDefault();
if (themetype != null)
{
var themeobject = Activator.CreateInstance(themetype);
theme = (Theme)themetype.GetProperty("Theme").GetValue(themeobject);
var themeobject = Activator.CreateInstance(themetype) as ITheme;
theme = themeobject.Theme;
}
else
{

View File

@ -1,6 +1,6 @@
/*
migration script
Version 0.9.1 migration script
*/

View File

@ -0,0 +1,18 @@
/*
Version 0.9.2 migration script
*/
ALTER TABLE [dbo].[Role]
ALTER COLUMN [Description] VARCHAR (256) NOT NULL
GO
ALTER TABLE [dbo].[Page] ADD
[DefaultContainerType] [nvarchar](200) NULL
GO
UPDATE [dbo].[Page]
SET [DefaultContainerType] = ''
GO

View File

@ -14,11 +14,13 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane
{
@ -26,6 +28,7 @@ namespace Oqtane
{
public IConfigurationRoot Configuration { get; }
private string _webRoot;
private Runtime _runtime;
public Startup(IWebHostEnvironment env)
{
@ -33,6 +36,9 @@ namespace Oqtane
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
Configuration = builder.Build();
_runtime = (Configuration.GetSection("Runtime").Value == "WebAssembly") ? Runtime.WebAssembly : Runtime.Server;
_webRoot = env.WebRootPath;
AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(env.ContentRootPath, "Data"));
}
@ -157,7 +163,7 @@ namespace Oqtane
services.AddSingleton<IDatabaseManager, DatabaseManager>();
// install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain )
InstallationManager.UnpackPackages("Modules,Themes", _webRoot);
InstallationManager.InstallPackages("Modules,Themes", _webRoot);
// register transient scoped core services
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
@ -187,14 +193,13 @@ namespace Oqtane
services.AddTransient<ISqlRepository, SqlRepository>();
services.AddTransient<IUpgradeManager, UpgradeManager>();
// load the external assemblies into the app domain
services.AddOqtaneParts();
// load the external assemblies into the app domain, install services
services.AddOqtaneParts(_runtime);
services.AddMvc()
.AddNewtonsoftJson()
.AddOqtaneApplicationParts() // register any Controllers from custom modules
.AddNewtonsoftJson();
.ConfigureOqtaneMvc(); // any additional configuration from IStart classes.
services.AddSwaggerGen(c =>
{
@ -217,14 +222,12 @@ namespace Oqtane
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
@ -237,6 +240,7 @@ namespace Oqtane
endpoints.MapControllers();
endpoints.MapFallbackToPage("/_Host");
});
app.ConfigureOqtaneAssemblies(env);
}
}
}

View File

@ -10,7 +10,7 @@
<body>
<div>
<br /><br /><h1 align="center">Please Wait... Upgrade In Progress....</h1>
<img src="https://www.oqtane.org/Portals/0/oqtane.png" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);" />
<img src="https://www.oqtane.org/Portals/0/oqtane-black.png" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);" />
</div>
</body>
</html>

View File

@ -0,0 +1 @@
/* HtmlText Module Custom Styles */

View File

@ -101,6 +101,12 @@
flex-direction: row;
}
.app-logo {
display: block;
margin-left: auto;
margin-right: auto;
}
.breadcrumbs {
position: fixed;
left: 275px;
@ -163,7 +169,13 @@
}
}
@media (max-width: 767px) {
@media (max-width: 767px) {
.app-logo {
height: 80px;
display: flex;
align-items: center;
}
.breadcrumbs {
position: fixed;
top: 150px;

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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