diff --git a/Directory.Packages.props b/Directory.Packages.props index 1157509643..ecb4471b60 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -123,7 +123,7 @@ - + diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json index a5665a1215..cd78818f8c 100644 --- a/docs/en/docs-params.json +++ b/docs/en/docs-params.json @@ -12,6 +12,17 @@ "NG": "Angular" } }, + { + "name": "BlazorUI", + "displayName": "Blazor UI Library", + "values": { + "Blazorise": "Blazorise", + "MudBlazor": "MudBlazor" + }, + "dependsOn": { + "UI": ["Blazor", "BlazorServer", "BlazorWebApp", "MAUIBlazor"] + } + }, { "name": "DB", "displayName": "Database", diff --git a/docs/en/framework/infrastructure/text-templating/razor.md b/docs/en/framework/infrastructure/text-templating/razor.md index d2f690c72c..a8bc1c55a6 100644 --- a/docs/en/framework/infrastructure/text-templating/razor.md +++ b/docs/en/framework/infrastructure/text-templating/razor.md @@ -10,6 +10,8 @@ The Razor template is a standard C# class, so you can freely use the functions of C#, such as `dependency injection`, using `LINQ`, custom methods, and even using `Repository`. +> The Razor engine compiles template content into a fully-trusted .NET assembly via Roslyn and executes it in the host process, so editing a Razor template at runtime is functionally equivalent to executing arbitrary server-side code. `RazorTemplateRenderingEngine.IsSandboxed` is therefore `false`, and the [Text Template Management](../../../modules/text-template-management.md) module requires the `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` permission (in addition to `EditContents`) before allowing such templates to be edited via its UI. Grant the related permission only to fully trusted developers/operators. If you need a sandboxed engine for content editors, consider [Scriban](scriban.md), which is configured to honor Scriban's [safe runtime boundaries](https://github.com/scriban/scriban/blob/master/site/docs/runtime/safe-runtime.md) by default. + ## Installation diff --git a/docs/en/framework/infrastructure/text-templating/scriban.md b/docs/en/framework/infrastructure/text-templating/scriban.md index bb72624294..0748f859d7 100644 --- a/docs/en/framework/infrastructure/text-templating/scriban.md +++ b/docs/en/framework/infrastructure/text-templating/scriban.md @@ -7,6 +7,31 @@ # Scriban Integration +## Safe Runtime (Sandbox) + +Scriban's [safe runtime](https://github.com/scriban/scriban/blob/master/site/docs/runtime/safe-runtime.md) builds the practical sandbox out of four boundaries: which globals you expose through `ScriptObject`, which .NET members you allow through the member filter, whether you configure `TemplateContext.TemplateLoader` for `include`, and which `TemplateContext` execution limits you enable. ABP's `ScribanTemplateRenderingEngine` is configured to honor these boundaries by default: + +| Boundary | ABP default | +|----------|-------------| +| Globals exposed | Only the `globalContext` (`Dictionary`) entries, the `model` you pass to `RenderAsync`, and the `L` localization helper. | +| .NET member access | `TemplateContext.MemberFilter` is set to `IsMemberAllowed`, an allowlist that exposes public properties only. Methods, fields, events, and `object`-level members (`GetType`, `ToString`, ...) are not reachable, which closes reflection-based escape paths such as `{{ model.GetType.Assembly.GetType "..." }}`. | +| `TemplateLoader` | Not configured. `include` directives have no template loader and cannot read templates from disk or other sources unless you explicitly wire one up. | +| Execution limits | Scriban's defaults (`LoopLimit = 1000`, `RecursiveLimit = 100`, `LimitToString = 1 MB`, `RegexTimeOut = 10s`). Override `CreateScribanTemplateContext` to tighten these for your own scenarios. | + +The recommended way to expose data to a Scriban template is via `ScriptObject` or `IDictionary` — the keys you put there are exactly what the template can see. When you pass a .NET object as `model`, the `MemberFilter` ensures only properties are exposed, but the safest pattern is to pre-build a dictionary or `ScriptObject` so the surface is fully under your control: + +````csharp +await _templateRenderer.RenderAsync( + "MyTemplate", + model: new Dictionary + { + { "name", user.Name }, + { "email", user.Email } + }); +```` + +If you must pass a .NET object whose methods/fields the template needs to read, override `ScribanTemplateRenderingEngine.IsMemberAllowed` to relax the filter. Only do so when the model objects are trusted and do not carry secrets, since methods and reflection entry points become reachable to whoever can edit the template content. + ## Installation It is suggested to use the [ABP CLI](../../../cli) to install this package. diff --git a/docs/en/framework/ui/blazor/basic-theme.md b/docs/en/framework/ui/blazor/basic-theme.md index 33c0cb7175..f8cd0c7023 100644 --- a/docs/en/framework/ui/blazor/basic-theme.md +++ b/docs/en/framework/ui/blazor/basic-theme.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -20,6 +21,18 @@ The Basic Theme is a theme implementation for the Blazor UI. It is a minimalist > See the [Theming document](theming.md) to learn about themes. +{{if BlazorUI == "MudBlazor"}} + +> **MudBlazor Variant** — When the `--blazor-ui-library mudblazor` option is used, the Basic Theme ships as a MudBlazor variant. Replace `BasicTheme` with `MudBlazorBasicTheme` everywhere in this document (package names, module type names and namespaces). The MudBlazor variant is **not** based on Bootstrap — it uses MudBlazor's Material Design layout components. +> +> Concrete package names you will see when using the MudBlazor variant: +> +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorBasicTheme` +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorBasicTheme.Bundling` +> * Module types: `Abp{...}MudBlazorBasicThemeModule`, `Abp{...}MudBlazorBasicThemeBundlingModule` + +{{end}} + ## Installation If you need to manually this theme, follow the steps below: diff --git a/docs/en/framework/ui/blazor/components/submit-button.md b/docs/en/framework/ui/blazor/components/submit-button.md index 832554c9b1..82dbeeeee9 100644 --- a/docs/en/framework/ui/blazor/components/submit-button.md +++ b/docs/en/framework/ui/blazor/components/submit-button.md @@ -1,12 +1,21 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { - "Description": "Explore the `SubmitButton` component in Blazor UI, designed for easy form submissions with localization support and loading indicators." + "Description": "Explore the submit button component in Blazor UI, designed for easy form submissions with localization support and loading indicators." } ``` # Blazor UI: SubmitButton Component +{{if BlazorUI == "Blazorise"}} + `SubmitButton` is a simple wrapper around `Button` component. It is used to be placed inside of page Form or Modal dialogs where it can response to user actions and to be activated as a default button by pressing an ENTER key. Once clicked it will go into the `disabled` state and also it will show a small loading indicator until clicked event is finished. ## Quick Example @@ -29,4 +38,54 @@ Notice that we didn't specify any text, like `Save Changes`. This is because `Su @L["Save"] -``` \ No newline at end of file +``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +The MudBlazor variant of ABP UI does not ship a dedicated `SubmitButton` wrapper. Use the standard `MudButton` together with the typical `Processing`/`Disabled` pattern to disable the button and show a progress indicator while the click handler is running. + +## Quick Example + +```razor + + @if (_processing) + { + + } + @L["Save"] + + +@code { + private bool _processing; + + private async Task SaveAsync() + { + _processing = true; + try + { + // ... your save operation + } + finally + { + _processing = false; + } + } +} +``` + +## Submit on Enter + +When the button is placed inside a `` or a ``, pressing ENTER inside an input control submits the form. To run validation before saving, call `_form.Validate()` first. See the [Forms & Validation](../forms-validation.md) page for details. + +## Use Inside `AbpMudCrudPageBase` + +The MudBlazor CRUD page base (`AbpMudCrudPageBase`) already wires up the standard create/update buttons inside its dialogs and shows a progress indicator while the application service call is running. In most cases you don't need to author a save button by hand; override `OnCreatingEntityAsync` / `OnUpdatingEntityAsync` instead. + +> Check the [MudBlazor button documentation](https://mudblazor.com/components/button) for all available options. + +{{end}} \ No newline at end of file diff --git a/docs/en/framework/ui/blazor/customization-overriding-components.md b/docs/en/framework/ui/blazor/customization-overriding-components.md index 8855ff89bd..c919ed084e 100644 --- a/docs/en/framework/ui/blazor/customization-overriding-components.md +++ b/docs/en/framework/ui/blazor/customization-overriding-components.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -41,6 +42,8 @@ The next step is to create a razor component, like `MyBranding.razor`, in your a The content of the `MyBranding.razor` is shown below: +{{if BlazorUI == "Blazorise"}} + ````html @using Volo.Abp.DependencyInjection {{if UI == "BlazorServer"}} @@ -58,6 +61,33 @@ The content of the `MyBranding.razor` is shown below: ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +The MudBlazor variant uses the LeptonX-based MudBlazor theme by default. The component to override is the `Branding` component shipped by the active MudBlazor theme: + +````html +@using Volo.Abp.DependencyInjection +{{if UI == "BlazorServer"}} +@using Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} +{{if UI == "Blazor"}} +@using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} + +@inherits Branding +@attribute [ExposeServices(typeof(Branding))] +@attribute [Dependency(ReplaceServices = true)] + + + +```` + +> If you are using the MudBlazor BasicTheme or a different MudBlazor theme, replace the namespace with the namespace of that theme's `Themes/` folder. + +{{end}} + Let's explain the code: * `@inherits Branding` line inherits the Branding component defined by the [Basic Theme](basic-theme.md) (in the {{if UI == "BlazorServer"}}`Volo.Abp.AspNetCore.Components.Server.BasicTheme.Themes.Basic`{{end}} {{if UI == "Blazor"}}`Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic`{{end}} namespace). @@ -75,6 +105,8 @@ Now, you can run the application to see the result: If you prefer to use code-behind file for the C# code of your component, you can use the attributes in the C# side. +{{if BlazorUI == "Blazorise"}} + **MyBlazor.razor** ````html @@ -113,6 +145,50 @@ namespace MyProject.Blazor.Components } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +**MyBlazor.razor** + +````html +{{if UI == "BlazorServer"}} +@using Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} +{{if UI == "Blazor"}} +@using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} +@inherits Branding + + + +```` + +**MyBlazor.razor.cs** + +````csharp +{{if UI == "BlazorServer"}} +using Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX; +{{end}} +{{if UI == "Blazor"}} +using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX; +{{end}} + +using Volo.Abp.DependencyInjection; + +namespace MyProject.Blazor.Components +{ + [ExposeServices(typeof(Branding))] + [Dependency(ReplaceServices = true)] + public partial class MyBranding + { + + } +} +```` + +{{end}} + ## Theming The [Theming](theming.md) system allows you to build your own theme. You can create your theme from scratch or get the [Basic Theme](basic-theme.md) and change however you like. diff --git a/docs/en/framework/ui/blazor/data-table-column-extensions.md b/docs/en/framework/ui/blazor/data-table-column-extensions.md index b89b825201..bb6bc6eeb0 100644 --- a/docs/en/framework/ui/blazor/data-table-column-extensions.md +++ b/docs/en/framework/ui/blazor/data-table-column-extensions.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -102,6 +109,8 @@ public class CustomTableColumn Navigate to the razor file and paste the following code. +{{if BlazorUI == "Blazorise"}} + ```csharp @using System @using Volo.Abp.Identity @@ -116,6 +125,27 @@ else } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@using Volo.Abp.Identity + +@if (Data.As().EmailConfirmed) +{ + +} +else +{ + +} +``` + +> When using MudBlazor, the standard data grid in module pages is `AbpMudExtensibleDataGrid`. You can replace `Component = typeof(CustomTableColumn)` exactly the same way as in Blazorise; the column system is shared across both UI libraries. + +{{end}} + Navigate back to the `CustomizedUserManagement` class, and use `Component` property to specify the custom blazor component. ```csharp diff --git a/docs/en/framework/ui/blazor/entity-action-extensions.md b/docs/en/framework/ui/blazor/entity-action-extensions.md index ab3d4b3cba..78d16f7327 100644 --- a/docs/en/framework/ui/blazor/entity-action-extensions.md +++ b/docs/en/framework/ui/blazor/entity-action-extensions.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -95,6 +102,8 @@ Here, the list of the properties that you use in the `EntityAction`. #### Example +{{if BlazorUI == "Blazorise"}} + ```csharp var clickMeAction = new EntityAction() { @@ -115,3 +124,33 @@ var clickMeAction = new EntityAction() } }; ``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +var clickMeAction = new EntityAction() +{ + Text = "Click Me!", + Clicked = (data) => + { + //TODO: Write your custom code + + return Task.CompletedTask; + }, + Color = MudBlazor.Color.Error, + Icon = MudBlazor.Icons.Material.Filled.PanTool, + ConfirmationMessage = (data) => "Are you sure you want to click to the action?", + Visible = (data) => + { + //TODO: Write your custom visibility action + //var selectedUser = data.As(); + return true; + } +}; +``` + +> The MudBlazor variant uses `MudBlazor.Color` enum values (e.g. `Color.Primary`, `Color.Error`, `Color.Success`) for `Color`, and Material Icon constants (e.g. `Icons.Material.Filled.Edit`) for `Icon`. The `EntityAction` model itself is shared with Blazorise; only the values you put inside it change. + +{{end}} diff --git a/docs/en/framework/ui/blazor/error-handling.md b/docs/en/framework/ui/blazor/error-handling.md index 2fae4ff38d..211c56f25e 100644 --- a/docs/en/framework/ui/blazor/error-handling.md +++ b/docs/en/framework/ui/blazor/error-handling.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -36,7 +37,9 @@ There are different type of `Exception` classes handled differently by the ABP. **Example** -````csharp +{{if BlazorUI == "Blazorise"}} + +````razor @page "/" @using Volo.Abp @@ -60,11 +63,41 @@ There are different type of `Exception` classes handled differently by the ABP. {{end}} +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/" +@using Volo.Abp + +Throw test exception + +@code +{ + private async Task TestException() + { + try + { + throw new UserFriendlyException("A user friendly error message!"); + } + catch(UserFriendlyException ex) + { + await HandleErrorAsync(ex); + } + } +} +```` + +{{end}} + +{{end}} + {{if UI == "Blazor"}} **Example** -````csharp +{{if BlazorUI == "Blazorise"}} + +````razor @page "/" @using Volo.Abp @@ -78,6 +111,28 @@ There are different type of `Exception` classes handled differently by the ABP. } } ```` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/" +@using Volo.Abp + +Throw test exception + +@code +{ + private void TestException() + { + throw new UserFriendlyException("A user friendly error message!"); + } +} +```` + +{{end}} + {{end}} ABP automatically handle the exception and show an error message to the user: diff --git a/docs/en/framework/ui/blazor/forms-validation.md b/docs/en/framework/ui/blazor/forms-validation.md index 7fb1423e77..c7f799c484 100644 --- a/docs/en/framework/ui/blazor/forms-validation.md +++ b/docs/en/framework/ui/blazor/forms-validation.md @@ -1,12 +1,21 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { - "Description": "Learn how to implement form validation in ABP Blazor UI using Blazorise's validation infrastructure with practical examples." + "Description": "Learn how to implement form validation in ABP Blazor UI using Blazorise or MudBlazor with practical examples." } ``` # Blazor UI: Forms & Validation +{{if BlazorUI == "Blazorise"}} + ABP Blazor UI is based on the [Blazorise](https://blazorise.com/docs) and does not have a built-in form validation infrastructure. However, you can use the [Blazorise validation infrastructure](https://blazorise.com/docs/components/validation) to validate your forms. ## Sample @@ -44,4 +53,89 @@ _The example is provided by official Blazorise documentation._ } ``` -> Check the [Blazorise documentation](https://blazorise.com/docs/components/validation) for more information and examples. \ No newline at end of file +> Check the [Blazorise documentation](https://blazorise.com/docs/components/validation) for more information and examples. + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +ABP Blazor UI built on top of [MudBlazor](https://mudblazor.com) uses MudBlazor's built-in form components and validation infrastructure. MudBlazor accepts a `ValidationAttribute` (e.g. `[Required]`, `[EmailAddress]` from ASP.NET Core's `DataAnnotations`) on the input's `Validation` parameter, plus custom `Func` / `Func>` delegates. FluentValidation can be plugged in the same way. + +## Sample + +The most common pattern is wrapping inputs in a `` and binding the form's validation state through `IsValid`: + +> Standard MudBlazor and ABP usings (`@using MudBlazor`, `@using Volo.Abp.MudBlazorUI`, etc.) come from the project's `_Imports.razor`. The example below only adds the additional usings needed for validation. + +```razor +@using System.ComponentModel.DataAnnotations + + + + + + + + + Submit + + + + +@code { + private MudForm _form; + private bool _isValid; + private SampleModel _model = new(); + + private async Task SubmitAsync() + { + await _form.Validate(); + if (_isValid) + { + // ... + } + } + + public class SampleModel + { + [Required] + public string Name { get; set; } + + [Required, EmailAddress] + public string Email { get; set; } + } +} +``` + +### Inputs Used in CRUD Pages + +ABP's MudBlazor CRUD pages (see `AbpMudCrudPageBase`) use a `` containing a `` and standard MudBlazor inputs: + +* `` / `` for text and multi-line text +* `` / `` for dropdowns +* `` for booleans +* `` / `` for date and time +* `` for numbers + +`AbpMudCrudPageBase.CreateEntityAsync` and `UpdateEntityAsync` validate the form for you (`CreateFormRef.Validate()` / `EditFormRef.Validate()`) and only call the corresponding hook when the form is valid. To inject custom logic before the application service call, override `OnCreatingEntityAsync` / `OnUpdatingEntityAsync` (do **not** re-validate inside the override): + +```csharp +protected override Task OnCreatingEntityAsync() +{ + // mutate NewEntity here if needed + return base.OnCreatingEntityAsync(); +} +``` + +> Check the [MudBlazor documentation](https://mudblazor.com/components/form) for the full list of validation modes and the [MudBlazor inputs reference](https://mudblazor.com/components/textfield). + +{{end}} \ No newline at end of file diff --git a/docs/en/framework/ui/blazor/overall.md b/docs/en/framework/ui/blazor/overall.md index 23077e307e..f5d5cf7b78 100644 --- a/docs/en/framework/ui/blazor/overall.md +++ b/docs/en/framework/ui/blazor/overall.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -95,6 +102,8 @@ Currently, three themes are **officially provided**: There are a set of standard libraries that comes pre-installed and supported by all the themes: +{{if BlazorUI == "Blazorise"}} + * [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. * [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree. * [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. @@ -106,6 +115,22 @@ These libraries are selected as the base libraries and available to the applicat > Beginning from June, 2021, the Blazorise library has dual licenses; open source & commercial. Based on your yearly revenue, you may need to buy a commercial license. See [this post](https://blazorise.com/news/announcing-2022-blazorise-plans-and-pricing-updates) to learn more. The Blazorise license is bundled with ABP and commercial customers doesn't need to buy an extra Blazorise license. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +* [MudBlazor](https://mudblazor.com/) as the component library, providing a complete set of Material Design components built natively for Blazor (form controls, data grid, dialogs, snackbars, dates, etc.). +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [Flag Icon](https://github.com/lipis/flag-icons) as a library to show flags of countries. + +These libraries are selected as the base libraries and available to the applications and modules. + +The MudBlazor variant ships its own theming, dialog, snackbar and popover providers (see [Theming](theming.md)). The MudBlazor library is MIT-licensed and is bundled with ABP at no extra cost. + +> Bootstrap is **not** required when using MudBlazor; MudBlazor brings its own layout and component styles. + +{{end}} + ### The Layout The themes provide the layout. So, you have a responsive layout with the standard features already implemented. The screenshot below has taken from the layout of the [Basic Theme](basic-theme.md): diff --git a/docs/en/framework/ui/blazor/page-header.md b/docs/en/framework/ui/blazor/page-header.md index ed2e16903d..5c03af6aa4 100644 --- a/docs/en/framework/ui/blazor/page-header.md +++ b/docs/en/framework/ui/blazor/page-header.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -30,6 +37,8 @@ Breadcrumbs can be added using the `BreadcrumbItems` property. **Example: Add Language Management to the breadcrumb items.** +{{if BlazorUI == "Blazorise"}} + Create a collection of `Volo.Abp.BlazoriseUI.BreadcrumbItem` objects and set the collection to the `BreadcrumbItems` parameter. ```csharp @@ -44,6 +53,31 @@ public partial class Index } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Create a collection of `MudBlazor.BreadcrumbItem` objects and set the collection to the `BreadcrumbItems` parameter. The MudBlazor `BreadcrumbItem` constructor takes `(string text, string href, bool disabled = false, string icon = null)`. + +```csharp +using MudBlazor; + +public partial class Index +{ + protected List BreadcrumbItems { get; } = new(); + + protected override void OnInitialized() + { + BreadcrumbItems.Add(new BreadcrumbItem( + text: "Language Management", + href: null, + disabled: true)); + } +} +``` + +{{end}} + Navigate back to the razor page. ```csharp @@ -57,19 +91,38 @@ The theme then renders the breadcrumb. An example render result can be: * The Home icon is rendered by default. Set `BreadcrumbShowHome` to `false` to hide it. * Breadcrumb items will be activated based on current navigation. Set `BreadcrumbShowCurrent` to `false` to disable it. -You can add as many items as you need. `BreadcrumbItem` constructor gets three parameters: +You can add as many items as you need. + +{{if BlazorUI == "Blazorise"}} + +The `Volo.Abp.BlazoriseUI.BreadcrumbItem` constructor gets three parameters: * `text`: The text to show for the breadcrumb item. * `url` (optional): A URL to navigate to, if the user clicks to the breadcrumb item. * `icon` (optional): An icon class (like `fas fa-user-tie` for Font-Awesome) to show with the `text`. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +The `MudBlazor.BreadcrumbItem` constructor takes: + +* `text`: The text to show for the breadcrumb item. +* `href`: A URL to navigate to (use `null` for the current page). +* `disabled` (optional): When `true`, the item is rendered as the current/non-clickable item. +* `icon` (optional): A Material icon (e.g. `Icons.Material.Filled.Language`). + +{{end}} + ## Page Toolbar Page toolbar can be set using the `Toolbar` property. **Example: Add a "New Item" toolbar item to the page toolbar.** -Create a `PageToolbar` object and define toolbar items using the `AddButton` extension method. +Create a `PageToolbar` object and define toolbar items using the `AddButton` extension method. + +{{if BlazorUI == "Blazorise"}} ```csharp public partial class Index @@ -87,6 +140,28 @@ public partial class Index } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +public partial class Index +{ + protected PageToolbar Toolbar { get; } = new(); + + protected override void OnInitialized() + { + Toolbar.AddButton("New Item", () => + { + //Write your click action here + return Task.CompletedTask; + }, icon: MudBlazor.Icons.Material.Filled.Add); + } +} +``` + +{{end}} + Navigate back to the razor page and set the `Toolbar` parameter. ```csharp diff --git a/docs/en/framework/ui/blazor/page-layout.md b/docs/en/framework/ui/blazor/page-layout.md index 75666fd17a..abd32745d0 100644 --- a/docs/en/framework/ui/blazor/page-layout.md +++ b/docs/en/framework/ui/blazor/page-layout.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -36,6 +43,8 @@ Indicates current selected menu item name. Menu item name should match a unique Menu item name can be set on runtime too. +{{if BlazorUI == "Blazorise"}} + ```html @inject PageLayout PageLayout @@ -49,6 +58,25 @@ Menu item name can be set on runtime too. } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@inject PageLayout PageLayout + +Change Menu + +@code{ + protected void SetCategoriesMenuAsSelected() + { + PageLayout.MenuItemName = "MyProjectName.Categories"; + } +} +``` + +{{end}} + ![leptonx selected menu item](../../../images/leptonx-selected-menu-item-example.gif) @@ -57,6 +85,9 @@ Menu item name can be set on runtime too. ## BreadCrumbs BreadCrumbItems are used to render breadcrumbs in the PageHeader. + +{{if BlazorUI == "Blazorise"}} + ```csharp @inject PageLayout PageLayout @@ -65,6 +96,21 @@ BreadCrumbItems are used to render breadcrumbs in the PageHeader. } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@using MudBlazor +@inject PageLayout PageLayout + +@{ + PageLayout.BreadcrumbItems.Add(new BreadcrumbItem("My Page", "/my-page")); +} +``` + +{{end}} + ## Toolbar ToolbarItems are used to render action toolbar items in the PageHeader. diff --git a/docs/en/framework/ui/blazor/page-toolbar-extensions.md b/docs/en/framework/ui/blazor/page-toolbar-extensions.md index 9c96bf90f7..96c48b000d 100644 --- a/docs/en/framework/ui/blazor/page-toolbar-extensions.md +++ b/docs/en/framework/ui/blazor/page-toolbar-extensions.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -27,6 +34,8 @@ We will use the [component override system](customization-overriding-components. Here, the content of the overridden `SetToolbarItemsAsync` method. +{{if BlazorUI == "Blazorise"}} + ```csharp protected override async ValueTask SetToolbarItemsAsync() { @@ -38,10 +47,31 @@ protected override async ValueTask SetToolbarItemsAsync() }, "file-import", Blazorise.Color.Secondary); } ``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +protected override async ValueTask SetToolbarItemsAsync() +{ + await base.SetToolbarItemsAsync(); + Toolbar.AddButton("Import users from excel", () => + { + //TODO: Write your custom code + return Task.CompletedTask; + }, MudBlazor.Icons.Material.Filled.Upload, MudBlazor.Color.Secondary); +} +``` + +{{end}} + > In order to use the `AddButton` extension method, you need to add a using statement for the `Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars` namespace. Here, the entire content of the file. +{{if BlazorUI == "Blazorise"}} + ```csharp using System.Threading.Tasks; using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars; @@ -67,6 +97,37 @@ namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity.Blazor.Pages.Identity; + +namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity +{ + [ExposeServices(typeof(UserManagement))] + [Dependency(ReplaceServices = true)] + public class CustomizedUserManagement : UserManagement + { + protected override async ValueTask SetToolbarItemsAsync() + { + await base.SetToolbarItemsAsync(); + Toolbar.AddButton("Import users from excel", () => + { + //TODO: Write your custom code + return Task.CompletedTask; + }, MudBlazor.Icons.Material.Filled.Upload, MudBlazor.Color.Secondary); + } + } +} +``` + +{{end}} + When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `Order` to set the order of the button component relative to the other components). ## Advanced Use Cases @@ -83,9 +144,21 @@ For this example, we've created a `MyToolbarComponent` component under the `/Pag `MyToolbarComponent.razor` content: -````csharp +{{if BlazorUI == "Blazorise"}} + +````razor ```` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +CLICK ME +```` + +{{end}} We will leave the `MyToolbarComponent.razor.cs` file empty. Then you can add the `MyToolbarComponent` to the user management page toolbar: diff --git a/docs/en/framework/ui/blazor/theming.md b/docs/en/framework/ui/blazor/theming.md index 7ded33608f..1fff0a057f 100644 --- a/docs/en/framework/ui/blazor/theming.md +++ b/docs/en/framework/ui/blazor/theming.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -52,6 +53,8 @@ All the themes must depend on the [Volo.Abp.AspNetCore.Components.Server.Theming {{end}} +{{if BlazorUI == "Blazorise"}} + * [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. * [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree. * [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. @@ -61,6 +64,31 @@ These libraries are selected as the base libraries and available to the applicat > Bootstrap's JavaScript part is not used since the Blazorise library already provides the necessary functionalities to the Bootstrap components in a native way. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +* [MudBlazor](https://mudblazor.com/) as the component library, providing a Material Design component set built natively for Blazor (form controls, data grid, dialogs, snackbars, dates, etc.). +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [Flag Icon](https://github.com/lipis/flag-icons) as a library to show flags of countries. + +These libraries are selected as the base libraries and available to the applications and modules. + +A theme using the MudBlazor variant must place the four MudBlazor providers in the layout root so dialogs, snackbars and popovers work everywhere: + +```razor + + + + + +@Body +``` + +The provided themes (`Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme`, `Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme`, etc.) ship these providers as part of their layout templates. + +{{end}} + ### The Layout All themes must define a layout for the application. The following image shows the user management page in the [Basic Theme](basic-theme.md) application layout: @@ -90,6 +118,8 @@ A theme is simply a Razor Class Library. The easiest way of creating a new theme is adding [Basic Theme Source Code](https://github.com/abpframework/abp/tree/dev/modules/basic-theme) module with source codes and customizing it. +{{if BlazorUI == "Blazorise"}} + {{if UI == "Blazor"}} ```bash abp add-package Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme --with-source-code --add-to-solution-file @@ -102,6 +132,24 @@ abp add-package Volo.Abp.AspNetCore.Components.Server.BasicTheme --with-source-c ``` {{end}} +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +{{if UI == "Blazor"}} +```bash +abp add-package Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme --with-source-code --add-to-solution-file +``` +{{end}} + +{{if UI == "BlazorServer"}} +```bash +abp add-package Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme --with-source-code --add-to-solution-file +``` +{{end}} + +{{end}} + ### Global Styles / Scripts A theme generally needs to add a global style to the page. ABP provides a system to manage the [Global Styles and Scripts](global-scripts-styles.md). A theme can implement the `IBundleContributor` to add global style or script files to the page. diff --git a/docs/en/modules/text-template-management.md b/docs/en/modules/text-template-management.md index fa62ee7470..32b7cc31a9 100644 --- a/docs/en/modules/text-template-management.md +++ b/docs/en/modules/text-template-management.md @@ -164,6 +164,22 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do See the `TextTemplateManagementPermissions` class members for all permissions defined for this module. +The module exposes two edit-time permissions with different risk levels: + +| Permission | Required to edit | Default grant | +|------------|------------------|---------------| +| `TextTemplateManagement.TextTemplates.EditContents` | Templates rendered by a sandboxed engine (e.g. Scriban). Editing such templates is safe for content editors because the engine cannot execute arbitrary .NET code. | Granted to roles that need to edit template text. | +| `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` | Templates rendered by a **non-sandboxed** engine (e.g. Razor). Editing such templates is functionally equivalent to granting server-side code execution because the engine compiles the content into a .NET assembly that runs with the host process's privileges. | **Not granted to any role by default**, including `admin`. Must be granted explicitly. | + +Whether a template is sandboxed is determined by `ITemplateRenderingEngine.IsSandboxed` on the engine that renders it. Editing a non-sandboxed template requires **both** `EditContents` and `EditNonSandboxedContents`. + +The Text Template Management UI surfaces this distinction: + +- A warning banner is rendered above the editor for non-sandboxed templates. +- The save and restore buttons are disabled when the current user lacks `EditNonSandboxedContents` for a non-sandboxed template. + +> Treat `EditNonSandboxedContents` as equivalent to granting shell access to the application server. Only assign it to fully trusted developers or operators. + ### Angular UI diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index 25fc187fa6..15fc138dc8 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -7,6 +7,12 @@ # Package Version Changes +## 10.4.0-rc.2 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| MongoDB.Driver | 3.8.0 | 3.8.1 | #25404 | + ## 10.4.0-rc.1 | Package | Old Version | New Version | PR | diff --git a/docs/en/release-info/migration-guides/abp-10-0.md b/docs/en/release-info/migration-guides/abp-10-0.md index 334adb2f4f..f3afb68d46 100644 --- a/docs/en/release-info/migration-guides/abp-10-0.md +++ b/docs/en/release-info/migration-guides/abp-10-0.md @@ -133,4 +133,28 @@ See the [Add failure retry policy to InboxProcessor](https://github.com/abpframe Starting from **ABP 10.0**, the [`HideErrors`](../../framework/fundamentals/caching#Available-Options) option of `AbpDistributedCacheOptions` is **disabled by default in the development environment**. By default, ABP hides and logs cache server errors to keep the application running even when the cache is unavailable. -However, in the **development environment**, errors are no longer hidden so that developers can immediately detect and fix **any cache server issues** (such as connection, configuration, or runtime errors). +However, in the **development environment**, errors are no longer hidden so that developers can immediately detect and fix **any cache server issues** (such as connection, configuration, or runtime errors). + +### Angular `LOGO_APP_NAME_TOKEN` May Need Explicit Provider in Module-based Apps + +In ABP v10, the logo/app-name binding moved to the `provideLogo(withEnvironmentOptions(...))` API from `@abp/ng.theme.shared`. + +For standalone Angular applications, configuring `provideLogo(withEnvironmentOptions(environment))` in `app.config.ts` is enough. + +For NgModule-based applications, some setups may still show the literal `ProjectName` text (the default value of `LOGO_APP_NAME_TOKEN`) instead of `environment.application.name`. In that case, provide the token explicitly in your module providers: + +```ts +import { LOGO_APP_NAME_TOKEN } from '@abp/ng.theme.shared'; +import { environment } from '../environments/environment'; + +@NgModule({ + // ... + providers: [ + // ... + { provide: LOGO_APP_NAME_TOKEN, useValue: environment.application.name }, + ], +}) +export class AppModule {} +``` + +For logo customization/replacement details, see the [Component Replacement](../../framework/ui/angular/component-replacement.md#how-to-replace-logocomponent) documentation. diff --git a/docs/en/release-info/migration-guides/abp-10-1.md b/docs/en/release-info/migration-guides/abp-10-1.md index ad0db21a37..2c973c28bb 100644 --- a/docs/en/release-info/migration-guides/abp-10-1.md +++ b/docs/en/release-info/migration-guides/abp-10-1.md @@ -60,6 +60,20 @@ ABP now targets Angular v21 (up from v20). For existing Angular projects, apply providers: [provideZoneChangeDetection(), ...appConfig.providers], }).catch(err => console.error(err)); ``` + + If you are using a module-based structure instead of a standalone one, update the providers in `app.module.ts`: + ```ts + @NgModule({ + declarations: [AppComponent], + providers: [ + //... + provideZoneChangeDetection(), + ], + bootstrap: [AppComponent], + }) + export class AppModule {} + ``` + - **tsconfig.json:** Align with the new property formats to avoid build issues: ```json /* angular/tsconfig.json */ diff --git a/docs/en/release-info/migration-guides/abp-10-4.md b/docs/en/release-info/migration-guides/abp-10-4.md index 213d8b7230..c66f09c96a 100644 --- a/docs/en/release-info/migration-guides/abp-10-4.md +++ b/docs/en/release-info/migration-guides/abp-10-4.md @@ -141,6 +141,87 @@ Configure(options => > See [#25235](https://github.com/abpframework/abp/pull/25235) for details. +### Text Template Rendering Engine — `IsSandboxed` Marker + +**Who is affected** + +- Custom rendering engines that implement `Volo.Abp.TextTemplating.ITemplateRenderingEngine` directly (not deriving from `TemplateRenderingEngineBase`). +- Modules and applications that surface template editing to non-developer users (e.g. the Text Template Management module). + +**What changed** + +- `ITemplateRenderingEngine` exposes a new required property: + + ```csharp + bool IsSandboxed { get; } + ``` + + Sandboxed engines (e.g. Scriban) interpret templates as a restricted DSL without .NET interop. Non-sandboxed engines (e.g. Razor) compile templates into fully-trusted .NET code that runs with the same privileges as the host process. +- `TemplateRenderingEngineBase` provides a virtual default of `false` (secure-by-default): engines that derive from the base class and don't override the property are treated as non-sandboxed. +- `RazorTemplateRenderingEngine` declares `IsSandboxed => false` (compiles to .NET assembly via Roslyn). +- `ScribanTemplateRenderingEngine` declares `IsSandboxed => true` and now sets Scriban's `TemplateContext.MemberFilter` so only public properties on imported objects are exposed; methods, fields, events and reflection entry points (`GetType`, `Assembly`, ...) are no longer reachable from Scriban templates. + +**What to do** + +- If your application registers a custom engine by implementing `ITemplateRenderingEngine` directly (without deriving from `TemplateRenderingEngineBase`), add the property: + + ```csharp + public bool IsSandboxed => false; // or true if your engine cannot execute host code + ``` + +- If your engine derives from `TemplateRenderingEngineBase`, no action is required for compilation; however, override `IsSandboxed => true` if your engine is genuinely sandboxed so callers (such as the Text Template Management module) treat its templates as safe to edit by non-developer users. +- Scriban templates that invoke methods on the model (e.g. `{%{{{ model.SomeMethod }}}%}`) or read fields stop working because the engine now whitelists public properties only. Templates that access only properties (the typical Scriban usage) are unaffected. To restore the previous behavior in custom hosts, derive from `ScribanTemplateRenderingEngine` and override `IsMemberAllowed` to allow methods or fields: + + ```csharp + protected override bool IsMemberAllowed(MemberInfo member) => true; + ``` + + Only do this when the model objects are trusted and do not carry secrets, since the previous behavior exposed reflection entry points (`GetType`, `Assembly`, ...) on imported .NET objects. + +> See [#25399](https://github.com/abpframework/abp/pull/25399) for details. + +### Text Template Management — `EditContents` Permission Split (security) + +**Who is affected** + +- Applications using the Text Template Management module that have granted the `TextTemplateManagement.TextTemplates.EditContents` permission to roles that are not fully trusted server administrators/developers. +- In particular, applications that use Razor templates (default for solutions referencing `Volo.Abp.TextTemplating.Razor`) and have any non-developer role with `EditContents`. + +**What changed** + +- A new permission `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` has been added. +- Editing a template whose rendering engine is **non-sandboxed** (i.e. `ITemplateRenderingEngine.IsSandboxed == false`, e.g. Razor) now requires **both** permissions: `EditContents` and `EditNonSandboxedContents`. +- The new permission is **not granted to any role by default**, including the `admin` role. The previously implicit assumption — that `EditContents` was enough to edit Razor templates — has been corrected: editing such templates is functionally equivalent to granting server-side code execution and is now gated by an explicit permission whose name communicates that risk. +- The Text Template Management UI (MVC, Blazor) renders a security warning banner when the current template's engine is non-sandboxed, and disables the save/restore buttons when the current user lacks the new permission. + +**What to do** + +After upgrading, audit which roles currently hold `EditContents` and decide which of them should also be granted `EditNonSandboxedContents`: + +1. Roles that should only edit sandboxed templates (e.g. Scriban, Liquid, plain HTML) need no further action — they keep editing those templates. +2. Roles that need to continue editing Razor templates must be granted `EditNonSandboxedContents` explicitly via the Permission Management page. +3. If — and only if — you have reviewed your role assignments and confirmed that every role currently holding `EditContents` is trusted to execute server-side code through Razor templates, you may seed the new permission for those roles via your `IDataSeedContributor`: + + ```csharp + var grants = await _permissionGrantRepository.GetListAsync( + TextTemplateManagementPermissions.TextTemplates.EditContents); + + foreach (var grant in grants) + { + await _permissionManager.SetAsync( + TextTemplateManagementPermissions.TextTemplates.EditNonSandboxedContents, + grant.ProviderName, + grant.ProviderKey, + isGranted: true); + } + ``` + +> Do not automate this seeding for arbitrary tenants/roles without first reviewing the current grants — auto-restoring the previously-implicit elevated trust would defeat the security improvement. The recommended path is to grant the new permission only to specific developer/operator roles via the Permission Management UI. + +If your application's data seeder grants all permissions in the `TextTemplateManagement` group to a role (e.g. the `admin` role), that role will automatically receive `EditNonSandboxedContents` on first run after upgrade. Audit your seeder if you want stricter defaults. + +> See the [Razor Integration](../../framework/infrastructure/text-templating/razor.md) document and [#25399](https://github.com/abpframework/abp/pull/25399) for details. + ### Dependency Updates **Who is affected** diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-05.md b/docs/en/tutorials/book-store-with-abp-suite/part-05.md index 73a1d9c348..865658d186 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-05.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-05.md @@ -11,7 +11,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp","NG","MAUIBlazor"], - "DB": ["EF", "Mongo"] + "DB": ["EF", "Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` diff --git a/docs/en/tutorials/book-store/part-02.md b/docs/en/tutorials/book-store/part-02.md index 1fe21c2848..429d7ec17c 100644 --- a/docs/en/tutorials/book-store/part-02.md +++ b/docs/en/tutorials/book-store/part-02.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp", "NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` ````json @@ -549,6 +550,8 @@ When you click on the Books menu item under the Book Store parent, you will be r ### Book List +{{if BlazorUI == "Blazorise"}} + We will use the [Blazorise library](https://blazorise.com/) as the UI component kit. It is a very powerful library that supports major HTML/CSS frameworks, including Bootstrap. ABP provides a generic base class - `AbpCrudPageBase<...>`, to create CRUD style pages. This base class is compatible with the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpCrudPageBase` to automate the code behind for the standard CRUD stuff. @@ -624,6 +627,81 @@ Open the `Books.razor` and replace the content as the following: While the code above is pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +We will use the [MudBlazor library](https://mudblazor.com/) as the UI component kit. It is a Material Design component library built natively for Blazor. + +ABP provides a generic base class — `AbpMudCrudPageBase<...>`, to create CRUD style pages. This base class is compatible with the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpMudCrudPageBase` to automate the code behind for the standard CRUD stuff. + +Open the `Books.razor` and replace the content as the following: + +````razor +@page "/books" +@using Volo.Abp.Application.Dtos +@using Acme.BookStore.Books +@using Acme.BookStore.Localization +@inherits AbpMudCrudPageBase + + + + + @L["Books"] + + + + + + + + + @L[$"Enum:BookType.{(int)context.Item.Type}"] + + + + + @context.Item.PublishDate.ToShortDateString() + + + + + + @context.Item.CreationTime.ToLongDateString() + + + + + + + +@code +{ + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + } +} +```` + +> If you see some syntax errors, you can ignore them if your application is properly built and running. Visual Studio still has some bugs with Blazor. + +* Inherited from `AbpMudCrudPageBase` which implements all the CRUD details for us. +* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class. +* `LocalizationResource` is set to the `BookStoreResource` to localize the texts. +* This page uses the standard MudBlazor `MudDataGrid` with `` definitions. ABP also ships an `AbpMudExtensibleDataGrid` that integrates with the [data table column extension system](../../framework/ui/blazor/data-table-column-extensions.md) when you need to extend module pages. + +While the code above is pretty easy to understand, you can check the MudBlazor [Card](https://mudblazor.com/components/card) and [DataGrid](https://mudblazor.com/components/datagrid) documents to understand them better. + +{{end}} + #### About the AbpCrudPageBase We will continue benefitting from `AbpCrudPageBase` for the books page. You could just inject the `IBookAppService` and perform all the server side calls yourself (thanks to the [Dynamic C# HTTP API Client Proxy](../../framework/api-development/dynamic-csharp-clients.md) system of the ABP). We will do it manually for the authors page to demonstrate how to call the server side HTTP APIs in your Blazor applications. diff --git a/docs/en/tutorials/book-store/part-03.md b/docs/en/tutorials/book-store/part-03.md index 73b2daea4e..259475b27d 100644 --- a/docs/en/tutorials/book-store/part-03.md +++ b/docs/en/tutorials/book-store/part-03.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -1094,6 +1095,8 @@ In this section, you will learn how to create a new modal dialog form to create ### Add a "New Button" Button +{{if BlazorUI == "Blazorise"}} + Open the `Books.razor` and replace the `` section with the following code: ````xml @@ -1110,6 +1113,27 @@ Open the `Books.razor` and replace the `` section with the following ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Open the `Books.razor` and replace the `` section with the following code: + +````razor + + + @L["Books"] + + + @L["NewBook"] + + +```` + +{{end}} + This will change the card header by adding a "New book" button to the right side: ![blazor-add-book-button](./images/blazor-add-book-button-2.png) @@ -1118,6 +1142,8 @@ Now, we can add a modal that will be opened when we click the button. ### Book Creation Modal +{{if BlazorUI == "Blazorise"}} + Open the `Books.razor` and add the following code to the end of the page: ````xml @@ -1184,6 +1210,57 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper` at * The form implements validation and the `AbpBlazorMessageLocalizerHelper` is used to simply localize the validation messages. * The `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` methods are defined by the base class. Check out the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Open the `Books.razor` and add the following code to the end of the page: + +````razor + + + @L["NewBook"] + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + +```` + +* The form uses `[Required]`/DataAnnotations for validation; messages are localized via the same `AbpResource` localization system. +* The `_createDialog` field, `CloseCreateDialogAsync`, `CreateFormRef` and `CreateEntityAsync` are all defined in `AbpMudCrudPageBase`. Check the [MudBlazor documentation](https://mudblazor.com/components/dialog) if you want to understand the `MudDialog` and other components. +* `MudDialog.Options` widens the dialog (`MaxWidth.Medium` + `FullWidth`) so the form fields are not cramped. +* `MudStack` with `Spacing="3"` keeps the inputs visually separated; without it MudBlazor inputs render flush against each other. +* `MudDatePicker.@bind-Date` requires a nullable `DateTime?`. If your DTO uses non-nullable `DateTime`, change it to `DateTime?` (`public DateTime? PublishDate { get; set; }`) when using the MudBlazor variant. + +{{end}} + That's all. Run the application and try to add a new book: ![blazor-new-book-modal](./images/blazor-new-book-modal-2.png) @@ -1194,6 +1271,8 @@ Editing a book is similar to creating a new book. ### Actions Dropdown +{{if BlazorUI == "Blazorise"}} + Open the `Books.razor` and add the following `DataGridEntityActionsColumn` section inside the `DataGridColumns` as the first item: ````xml @@ -1212,12 +1291,38 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it: +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Open the `Books.razor` and add the following `TemplateColumn` as the first column inside the `` section of the `MudDataGrid`: + +````razor + + + + + @L["Edit"] + + + + +```` + +* `OpenEditDialogAsync` is defined in the base class which takes the entity (book) to edit. + +This renders an "Actions" dropdown menu (`MudMenu`) for each row in the data grid. We will add the **Delete** menu item later in the *Deleting a Book* section. + +{{end}} + ![blazor-edit-book-action](./images/blazor-edit-book-action-3.png) ### Edit Modal We can now define a modal to edit the book. Add the following code to the end of the `Books.razor` page: +{{if BlazorUI == "Blazorise"}} + ````xml @@ -1273,6 +1378,49 @@ We can now define a modal to edit the book. Add the following code to the end of ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + + + @EditingEntity.Name + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + +```` + +{{end}} + ### Mapperly Configuration The base `AbpCrudPageBase` uses the [object to object mapping](../../framework/infrastructure/object-to-object-mapping.md) system to convert an incoming `BookDto` object to a `CreateUpdateBookDto` object. So, we need to define the mapping. @@ -1304,7 +1452,11 @@ You can now run the application and try to edit a book. ## Deleting a Book -Open the `Books.razor` page and add the following `EntityAction` code under the "Edit" action inside `EntityActions`: +Open the `Books.razor` page and add the following entity action code under the "Edit" action. + +{{if BlazorUI == "Blazorise"}} + +Add the following `EntityAction` code under the "Edit" action inside `EntityActions`: ````xml ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Add the following `MudMenuItem` after the "Edit" item inside the actions `MudMenu`: + +````razor + + @L["Delete"] + +```` + +{{end}} + * `DeleteEntityAsync` is defined in the base class that deletes the entity by performing a call to the server. * `ConfirmationMessage` is a callback to show a confirmation message before executing the action. * `GetDeleteConfirmationMessage` is defined in the base class. You can override this method (or pass another value to the `ConfirmationMessage` parameter) to customize the localization message. @@ -1327,6 +1498,8 @@ Run the application and try to delete a book. Here's the complete code to create the book management CRUD page, that has been developed in the last two parts: +{{if BlazorUI == "Blazorise"}} + ````xml @page "/books" @using Volo.Abp.Application.Dtos @@ -1521,3 +1694,142 @@ Here's the complete code to create the book management CRUD page, that has been {{end}} +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/books" +@using Volo.Abp.Application.Dtos +@using Acme.BookStore.Books +@using Acme.BookStore.Localization +@using Microsoft.Extensions.Localization +@inherits AbpMudCrudPageBase + + + + + @L["Books"] + + + @L["NewBook"] + + + + + + + + + @L["Edit"] + @L["Delete"] + + + + + + + @L[$"Enum:BookType.{(int)context.Item.Type}"] + + + + + @context.Item.PublishDate.ToShortDateString() + + + + + + @context.Item.CreationTime.ToLongDateString() + + + + + + + + + + @L["NewBook"] + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + + + + + @EditingEntity.Name + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + + +@code +{ + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + } +} +```` + +{{end}} + +{{end}} + diff --git a/docs/en/tutorials/book-store/part-09.md b/docs/en/tutorials/book-store/part-09.md index b70fd3e875..22c144dd14 100644 --- a/docs/en/tutorials/book-store/part-09.md +++ b/docs/en/tutorials/book-store/part-09.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -837,6 +838,8 @@ That's all! This is a fully working CRUD page, you can create, edit and delete a Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project with the following content: +{{if BlazorUI == "Blazorise"}} + ````xml @page "/authors" @using Acme.BookStore.Authors @@ -1017,11 +1020,133 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B ```` -* This code is similar to the `Books.razor`, except it doesn't inherit from the `AbpCrudPageBase`, but uses its own implementation. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/authors" +@using Acme.BookStore.Authors +@using Acme.BookStore.Localization +@inherits BookStoreComponentBase +@inject IAuthorAppService AuthorAppService + + + + + @L["Authors"] + + + @if (CanCreateAuthor) + { + + @L["NewAuthor"] + + } + + + + + + + + + @if (CanEditAuthor) + { + + @L["Edit"] + + } + @if (CanDeleteAuthor) + { + + @L["Delete"] + + } + + + + + + + @context.Item.BirthDate.ToShortDateString() + + + + + + + + + + @L["NewAuthor"] + + + + + + + + + + + + @L["Cancel"] + + @L["Save"] + + + + + + + @EditingAuthor.Name + + + + + + + + + + + + @L["Cancel"] + + @L["Save"] + + + +```` + +{{end}} + +* This code is similar to the `Books.razor`, except it doesn't inherit from the `AbpCrudPageBase`/`AbpMudCrudPageBase`, but uses its own implementation. * Injects the `IAuthorAppService` to consume the server side HTTP APIs from the UI. We can directly inject application service interfaces and use just like regular method calls by the help of [Dynamic C# HTTP API Client Proxy System](../../framework/api-development/dynamic-csharp-clients.md), which performs REST API calls for us. See the `Authors` class below to see the usage. Create a new code behind file, `Authors.razor.cs`, under the `Pages` folder, with the following content: +{{if BlazorUI == "Blazorise"}} + ````csharp using System; using System.Collections.Generic; @@ -1195,6 +1320,197 @@ public partial class Authors } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Permissions; +using Microsoft.AspNetCore.Authorization; +using MudBlazor; +using Volo.Abp.Application.Dtos; + +{{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor.Pages;{{ else if UI == "MAUIBlazor" }}namespace Acme.BookStore.MauiBlazor.Pages;{{ else }}namespace Acme.BookStore.Blazor.Client.Pages;{{ end }} + +public partial class Authors +{ + private IReadOnlyList AuthorList { get; set; } + + private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; + private int CurrentPage { get; set; } + private string CurrentSorting { get; set; } + private int TotalCount { get; set; } + + private bool CanCreateAuthor { get; set; } + private bool CanEditAuthor { get; set; } + private bool CanDeleteAuthor { get; set; } + + private CreateAuthorDto NewAuthor { get; set; } + + private Guid EditingAuthorId { get; set; } + private UpdateAuthorDto EditingAuthor { get; set; } + + private MudDialog CreateAuthorDialog { get; set; } + private MudDialog EditAuthorDialog { get; set; } + + private MudForm CreateFormRef; + private MudForm EditFormRef; + + // MudDatePicker requires nullable DateTime, while AuthorDto.BirthDate is non-nullable. + // Bind to a nullable wrapper and sync back to the DTO before saving. + private DateTime? NewAuthorBirthDate + { + get => NewAuthor?.BirthDate; + set { if (NewAuthor != null && value.HasValue) NewAuthor.BirthDate = value.Value; } + } + + private DateTime? EditingAuthorBirthDate + { + get => EditingAuthor?.BirthDate; + set { if (EditingAuthor != null && value.HasValue) EditingAuthor.BirthDate = value.Value; } + } + + public Authors() + { + NewAuthor = new CreateAuthorDto(); + EditingAuthor = new UpdateAuthorDto(); + } + + protected override async Task OnInitializedAsync() + { + await SetPermissionsAsync(); + await GetAuthorsAsync(); + } + + private async Task SetPermissionsAsync() + { + CanCreateAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Create); + + CanEditAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Edit); + + CanDeleteAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Delete); + } + + private async Task GetAuthorsAsync() + { + var result = await AuthorAppService.GetListAsync( + new GetAuthorListDto + { + MaxResultCount = PageSize, + SkipCount = CurrentPage * PageSize, + Sorting = CurrentSorting + } + ); + + AuthorList = result.Items; + TotalCount = (int)result.TotalCount; + } + + private async Task> OnDataGridReadAsync(GridState state) + { + CurrentSorting = state.SortDefinitions + .Where(s => !string.IsNullOrWhiteSpace(s.SortBy?.ToString())) + .Select(s => $"{s.SortBy}{(s.Descending ? " DESC" : "")}") + .JoinAsString(","); + CurrentPage = state.Page; + + await GetAuthorsAsync(); + + return new GridData { Items = AuthorList, TotalItems = TotalCount }; + } + + private async Task OpenCreateAuthorDialogAsync() + { + NewAuthor = new CreateAuthorDto(); + if (CreateFormRef != null) await CreateFormRef.ResetAsync(); + await InvokeAsync(() => CreateAuthorDialog.ShowAsync()); + } + + private Task CloseCreateAuthorDialogAsync() + { + return InvokeAsync(() => CreateAuthorDialog.CloseAsync()); + } + + private async Task OpenEditAuthorDialogAsync(AuthorDto author) + { + EditingAuthorId = author.Id; + EditingAuthor = ObjectMapper.Map(author); + if (EditFormRef != null) await EditFormRef.ResetAsync(); + await InvokeAsync(() => EditAuthorDialog.ShowAsync()); + } + + private Task CloseEditAuthorDialogAsync() + { + return InvokeAsync(() => EditAuthorDialog.CloseAsync()); + } + + private async Task DeleteAuthorAsync(AuthorDto author) + { + try + { + var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; + if (!await Message.Confirm(confirmMessage)) + { + return; + } + + await AuthorAppService.DeleteAsync(author.Id); + await GetAuthorsAsync(); + } + catch(Exception ex) + { + await HandleErrorAsync(ex); + } + } + + private async Task CreateAuthorAsync() + { + try + { + await CreateFormRef.Validate(); + if (CreateFormRef.IsValid) + { + await AuthorAppService.CreateAsync(NewAuthor); + await GetAuthorsAsync(); + await InvokeAsync(() => CreateAuthorDialog.CloseAsync()); + } + } + catch(Exception ex) + { + await HandleErrorAsync(ex); + } + } + + private async Task UpdateAuthorAsync() + { + try + { + await EditFormRef.Validate(); + if (EditFormRef.IsValid) + { + await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); + await GetAuthorsAsync(); + await InvokeAsync(() => EditAuthorDialog.CloseAsync()); + } + } + catch(Exception ex) + { + await HandleErrorAsync(ex); + } + } +} +```` + +{{end}} + This class typically defines the properties and methods used by the `Authors.razor` page. ### Object Mapping diff --git a/docs/en/tutorials/book-store/part-10.md b/docs/en/tutorials/book-store/part-10.md index e03ada5a96..f8ff85e5d5 100644 --- a/docs/en/tutorials/book-store/part-10.md +++ b/docs/en/tutorials/book-store/part-10.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -1115,7 +1116,9 @@ That's all. Just run the application and try to create or edit an author. ### The Book List -It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following `DataGridColumn` definition just after the `Name` (book name) column: +It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following column definition just after the `Name` (book name) column: + +{{if BlazorUI == "Blazorise"}} ````xml ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + +```` + +{{end}} + When you run the application, you can see the *Author* column on the table: ![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors-2.png) @@ -1147,6 +1160,8 @@ protected override async Task OnInitializedAsync() * It is essential to call the `base.OnInitializedAsync()` since `AbpCrudPageBase` has some initialization code to be executed. +{{if BlazorUI == "Blazorise"}} + Override the `OpenCreateModalAsync` method and adding the following code: ````csharp @@ -1199,7 +1214,67 @@ The final `@code` block should be the following: } ```` -Finally, add the following `Field` definition into the `ModalBody` of the *Create* modal, as the first item, before the `Name` field: +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Override the `OpenCreateDialogAsync` method and adding the following code: + +````csharp +protected override async Task OpenCreateDialogAsync() +{ + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateDialogAsync(); + NewEntity.AuthorId = authorList.First().Id; +} +```` + +The final `@code` block should be the following: + +````csharp +@code +{ + //ADDED A NEW FIELD + IReadOnlyList authorList = Array.Empty(); + + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Delete; + } + + //GET AUTHORS ON INITIALIZATION + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + authorList = (await AppService.GetAuthorLookupAsync()).Items; + } + + protected override async Task OpenCreateDialogAsync() + { + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateDialogAsync(); + NewEntity.AuthorId = authorList.First().Id; + } +} +```` + +{{end}} + +Finally, add the following field definition into the *Create* modal/dialog, as the first item, before the `Name` field: + +{{if BlazorUI == "Blazorise"}} ````xml @@ -1215,6 +1290,21 @@ Finally, add the following `Field` definition into the `ModalBody` of the *Creat ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + + @foreach (var author in authorList) + { + @author.Name + } + +```` + +{{end}} + This requires to add a new localization key to the `en.json` file: ````js @@ -1227,7 +1317,9 @@ You can run the application to see the *Author Selection* while creating a new b ### Edit Book Modal -Add the following `Field` definition into the `ModalBody` of the *Edit* modal, as the first item, before the `Name` field: +Add the following field definition into the *Edit* modal/dialog, as the first item, before the `Name` field: + +{{if BlazorUI == "Blazorise"}} ````xml @@ -1243,6 +1335,21 @@ Add the following `Field` definition into the `ModalBody` of the *Edit* modal, a ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + + @foreach (var author in authorList) + { + @author.Name + } + +```` + +{{end}} + That's all. We are reusing the `authorList` defined for the *Create* modal. {{end}} diff --git a/docs/en/tutorials/modular-crm/part-03.md b/docs/en/tutorials/modular-crm/part-03.md index e0db36d617..d34861a7ab 100644 --- a/docs/en/tutorials/modular-crm/part-03.md +++ b/docs/en/tutorials/modular-crm/part-03.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["MVC", "BlazorWebApp", "NG"] + "UI": ["MVC", "BlazorWebApp", "NG"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -515,6 +516,8 @@ Open the `ModularCrm.Catalog` .NET solution in your IDE, and find the `Pages/Cat Replace the `Index.razor` file with the following content: +{{if BlazorUI == "Blazorise"}} + ````razor @page "/catalog" @using System.Collections.Generic @@ -547,6 +550,44 @@ Replace the `Index.razor` file with the following content: } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/catalog" +@using System.Collections.Generic +@using System.Threading.Tasks +@using ModularCrm.Catalog +@inject IProductAppService ProductAppService + +Products + + + + + @foreach (var product in Products) + { + + @product.Name (stock: @product.StockCount) + + } + + + + +@code { + private List Products { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Products = await ProductAppService.GetListAsync(); + } +} +```` + +{{end}} + Here, you inject `IProductAppService`, get all products in `OnInitializedAsync`, and then render the result in a simple list. {{end}} diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index 4387ff6908..4f5b4fc2b5 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["MVC", "BlazorWebApp", "NG"] + "UI": ["MVC", "BlazorWebApp", "NG"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -525,6 +526,8 @@ public class OrderingMenuContributor : IMenuContributor Replace the `Index.razor` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.Blazor` project with the following code block: +{{if BlazorUI == "Blazorise"}} + ````razor @page "/ordering" @using System.Collections.Generic @@ -559,6 +562,46 @@ Replace the `Index.razor` content in the `Pages/Ordering` folder of the `Modular } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/ordering" +@using System.Collections.Generic +@using System.Threading.Tasks +@using ModularCrm.Ordering +@inject IOrderAppService OrderAppService + +Orders + + + + + @foreach (var order in Orders) + { + + Customer: @order.CustomerName
+ Product: @order.ProductId
+ State: @order.State +
+ } +
+
+
+ +@code { + private List Orders { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Orders = await OrderAppService.GetListAsync(); + } +} +```` + +{{end}} + This page shows a list of orders on the UI. You haven't created a UI to create new orders, and we will not do it to keep this tutorial simple. If you want to learn how to create advanced UIs with ABP, please follow the [Book Store tutorial](../book-store/index.md). ### Editing the Menu Item diff --git a/docs/en/tutorials/modular-crm/part-06.md b/docs/en/tutorials/modular-crm/part-06.md index 31cc0cb48c..1065e5df6e 100644 --- a/docs/en/tutorials/modular-crm/part-06.md +++ b/docs/en/tutorials/modular-crm/part-06.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["MVC", "BlazorWebApp", "NG"] + "UI": ["MVC", "BlazorWebApp", "NG"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -296,6 +297,8 @@ As you can see, we can see the product names instead of product IDs. Open the `Index.razor` file, and change the `@order.ProductId` part to `@order.ProductName` to write the product name instead of the product ID. The final `Index.razor` content should be the following: +{{if BlazorUI == "Blazorise"}} + ````razor @page "/ordering" @using System.Collections.Generic @@ -330,6 +333,46 @@ Open the `Index.razor` file, and change the `@order.ProductId` part to `@order.P } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/ordering" +@using System.Collections.Generic +@using System.Threading.Tasks +@using ModularCrm.Ordering +@inject IOrderAppService OrderAppService + +Orders + + + + + @foreach (var order in Orders) + { + + Customer: @order.CustomerName
+ Product: @order.ProductName
+ State: @order.State +
+ } +
+
+
+ +@code { + private List Orders { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Orders = await OrderAppService.GetListAsync(); + } +} +```` + +{{end}} + That's all. Now, you can graph build the main application and run it in ABP Studio to see the result: ![abp-studio-browser-list-of-orders-with-product-name](images/abp-studio-browser-list-of-orders-with-product-name.png) diff --git a/docs/en/tutorials/modular-crm/part-08.md b/docs/en/tutorials/modular-crm/part-08.md index f2739a53bb..2d911f93ec 100644 --- a/docs/en/tutorials/modular-crm/part-08.md +++ b/docs/en/tutorials/modular-crm/part-08.md @@ -176,8 +176,20 @@ Now, you know the fundamental principles and mechanics of building sophisticated ## Download the Source Code +{{if UI == "MVC"}} + +You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM). + +{{else if UI == "BlazorWebApp"}} + You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM-BlazorWebApp). +{{else if UI == "NG"}} + +You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/NG.ModularCRM). + +{{end}} + ## See Also See the following sections for additional resources. diff --git a/docs/en/tutorials/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index c8d95b627c..f0616c7765 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -11,7 +11,8 @@ //[doc-params] { "UI": ["MVC", "Blazor", "BlazorServer", "BlazorWebApp" ,"NG", "MAUIBlazor"], - "DB": ["EF", "Mongo"] + "DB": ["EF", "Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -637,6 +638,8 @@ See the *Dynamic C# Proxies & Auto API Controllers* section below to learn how w Open the `Index.razor` file in the `Pages` folder of the {{if UI=="Blazor" || UI=="BlazorWebApp"}} *TodoApp.Blazor.Client* {{else if UI=="BlazorServer"}} *TodoApp.Blazor* {{else if UI=="MAUIBlazor"}} *TodoApp.MauiBlazor* {{end}} project and replace the content with the following code block: +{{if BlazorUI == "Blazorise"}} + ```xml @page "/" @inherits TodoAppComponentBase @@ -675,6 +678,47 @@ Open the `Index.razor` file in the `Pages` folder of the {{if UI=="Blazor" || UI ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@page "/" +@inherits TodoAppComponentBase + + + + + + TODO LIST + + + + + + + Submit + + + + @foreach (var todoItem in TodoItems) + { + + + @todoItem.Text + + } + + + + +``` + +{{end}} + ### Index.razor.css As the final touch, open the `Index.razor.css` file in the `Pages` folder of the {{if UI=="Blazor" || UI=="BlazorWebApp"}}*TodoApp.Blazor.Client*{{else if UI=="BlazorServer"}} *TodoApp.Blazor* {{else if UI=="MAUIBlazor"}} *TodoApp.MauiBlazor* {{end}} project and add the following content: diff --git a/docs/en/tutorials/todo/single-layer/index.md b/docs/en/tutorials/todo/single-layer/index.md index 6a7f45ce62..b62abfb2b9 100644 --- a/docs/en/tutorials/todo/single-layer/index.md +++ b/docs/en/tutorials/todo/single-layer/index.md @@ -11,7 +11,8 @@ //[doc-params] { "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] + "DB": ["EF", "Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -621,6 +622,8 @@ This class uses the {{if UI=="Blazor"}}`ITodoAppService`{{else}}`TodoAppService` Open the `Index.razor` file in the {{if UI=="BlazorServer"}}`Components/Pages`{{else}}`Pages`{{end}} folder and replace the content with the following code block: +{{if BlazorUI == "Blazorise"}} + ```xml @page "/" @inherits TodoAppComponentBase @@ -660,6 +663,47 @@ Open the `Index.razor` file in the {{if UI=="BlazorServer"}}`Components/Pages`{{ ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@page "/" +@inherits TodoAppComponentBase + + + + + + TODO LIST + + + + + + + Submit + + + + @foreach (var todoItem in TodoItems) + { + + + @todoItem.Text + + } + + + + +``` + +{{end}} + ### Index.razor.css As the final touch, open the `Index.razor.css` file in the {{if UI=="BlazorServer"}}`Components/Pages`{{else}}`Pages`{{end}} folder and add the following code block at the end of the file: diff --git a/docs/en/ui-themes/basic-theme/index.md b/docs/en/ui-themes/basic-theme/index.md index c87491d1b4..71ee739721 100644 --- a/docs/en/ui-themes/basic-theme/index.md +++ b/docs/en/ui-themes/basic-theme/index.md @@ -19,6 +19,8 @@ See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to lea The Basic Theme has implementation for the following UI types: - [MVC UI](../../framework/ui/mvc-razor-pages/basic-theme.md) -- [Blazor UI](../../framework/ui/blazor/basic-theme.md) +- [Blazor UI](../../framework/ui/blazor/basic-theme.md) — available in two Blazor UI library variants: + - **Blazorise** (default): `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.BasicTheme` + - **MudBlazor**: `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorBasicTheme` - [Angular UI](../../framework/ui/angular/basic-theme.md) diff --git a/docs/en/ui-themes/index.md b/docs/en/ui-themes/index.md index 2beaa0dc05..36df6270ae 100644 --- a/docs/en/ui-themes/index.md +++ b/docs/en/ui-themes/index.md @@ -33,6 +33,15 @@ See the following documents based on the UI type you are using: - [Basic Theme - Blazor UI](../framework/ui/blazor/basic-theme.md) - [Basic Theme - Angular UI](../framework/ui/angular/basic-theme.md) +## Blazor UI Library + +When you create an ABP solution with a Blazor host (Blazor Server, Blazor WebAssembly or Blazor WebApp), you can additionally choose the underlying Blazor component library: + +* **Blazorise** — the original ABP default, based on Bootstrap. +* **MudBlazor** — a Material-Design component library, available as an alternative variant. Each official theme has a MudBlazor version (e.g. `MudBlazorLeptonXTheme`, `MudBlazorLeptonXLiteTheme`, `MudBlazorBasicTheme`). + +The choice is made at solution creation time via the `--blazor-ui-library` option (`abp new ... -bul mudblazor`) or in the ABP Studio new-solution wizard. + ## See Also * [Theming - MVC UI](../framework/ui/mvc-razor-pages/theming.md) diff --git a/docs/en/ui-themes/lepton-x-lite/blazor.md b/docs/en/ui-themes/lepton-x-lite/blazor.md index 7190ac1fc2..6b15e1f134 100644 --- a/docs/en/ui-themes/lepton-x-lite/blazor.md +++ b/docs/en/ui-themes/lepton-x-lite/blazor.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -20,6 +21,19 @@ LeptonX Lite has implementation for the ABP Blazor WebAssembly & Blazor Server. > See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to learn about themes. +{{if BlazorUI == "MudBlazor"}} + +> **MudBlazor Variant** — When the `--blazor-ui-library mudblazor` option is used, the LeptonX Lite theme ships as a MudBlazor variant. Replace `LeptonXLiteTheme` with `MudBlazorLeptonXLiteTheme` everywhere in this document (package names, module type names and namespaces). The installation steps, layout customization API and override mechanism are the same; only the package and namespace prefix change. The component implementations use MudBlazor primitives (`MudAppBar`, `MudDrawer`, `MudNavLink`, etc.) instead of Blazorise components. +> +> Concrete package names you will see when using the MudBlazor variant: +> +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXLiteTheme` +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXLiteTheme.Bundling` +> * Module types: `Abp{...}MudBlazorLeptonXLiteThemeModule`, `Abp{...}MudBlazorLeptonXLiteThemeBundlingModule` +> * Layout namespace: `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXLiteTheme.Themes.MudBlazorLeptonXLite` + +{{end}} + ## Installation This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: diff --git a/docs/en/ui-themes/lepton-x/blazor.md b/docs/en/ui-themes/lepton-x/blazor.md index 29e8deb6c3..07d07eea00 100644 --- a/docs/en/ui-themes/lepton-x/blazor.md +++ b/docs/en/ui-themes/lepton-x/blazor.md @@ -10,12 +10,26 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` LeptonX theme is implemented and ready to use with ABP. No custom implementation is needed for Blazor Server & WebAssembly. +{{if BlazorUI == "MudBlazor"}} + +> **MudBlazor Variant** — When the `--blazor-ui-library mudblazor` option is used, the LeptonX theme ships as a MudBlazor variant. Replace `LeptonXTheme` with `MudBlazorLeptonXTheme` everywhere in this document (package names, module type names and namespaces). The installation steps, layout customization API and override mechanism are the same; only the package and namespace prefix change. Component implementations use MudBlazor primitives (`MudAppBar`, `MudDrawer`, `MudNavLink`, `MudMenu`, etc.) instead of Blazorise components. +> +> Concrete package names you will see when using the MudBlazor variant: +> +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXTheme` +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXTheme.Bundling` +> * Module types: `Abp{...}MudBlazorLeptonXThemeModule`, `Abp{...}MudBlazorLeptonXThemeBundlingModule` +> * Layout components live under `Volo.Abp.AspNetCore.Components.{Web,Server,WebAssembly}.MudBlazorLeptonXTheme.Components.ApplicationLayout` (with `SideMenu`, `TopMenu`, and `PublicWebsiteLayout` sub-namespaces). + +{{end}} + ## Installation {{if UI == "Blazor"}} diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs index e100825011..e9f74595c8 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs @@ -9,6 +9,13 @@ public interface ITemplateRenderingEngine { string Name { get; } + /// + /// True when this engine restricts templates to a DSL without direct .NET + /// interop (e.g. Scriban). False when templates compile to fully-trusted + /// .NET code (e.g. Razor). + /// + bool IsSandboxed { get; } + /// /// Renders a text template. /// diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs index 62c8dee43c..7e5e10d86c 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs @@ -8,6 +8,8 @@ public abstract class TemplateRenderingEngineBase : ITemplateRenderingEngine { public abstract string Name { get; } + public virtual bool IsSandboxed => false; + protected readonly ITemplateDefinitionManager TemplateDefinitionManager; protected readonly ITemplateContentProvider TemplateContentProvider; protected readonly IStringLocalizerFactory StringLocalizerFactory; diff --git a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs index 485ca44121..b6c120fe9e 100644 --- a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs @@ -17,6 +17,8 @@ public class RazorTemplateRenderingEngine : TemplateRenderingEngineBase, ITransi public const string EngineName = "Razor"; public override string Name => EngineName; + public override bool IsSandboxed => false; + protected readonly IServiceScopeFactory ServiceScopeFactory; public RazorTemplateRenderingEngine( diff --git a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs index 7a5ac79592..9b7b3393a6 100644 --- a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Localization; @@ -14,6 +15,8 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran public const string EngineName = "Scriban"; public override string Name => EngineName; + public override bool IsSandboxed => true; + public ScribanTemplateRenderingEngine( ITemplateDefinitionManager templateDefinitionManager, ITemplateContentProvider templateContentProvider, @@ -118,7 +121,10 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran Dictionary globalContext, object? model = null) { - var context = new TemplateContext(); + var context = new TemplateContext + { + MemberFilter = IsMemberAllowed + }; var scriptObject = new ScriptObject(); @@ -140,4 +146,12 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran return context; } + + /// + /// Scriban member filter: only public properties on imported objects are exposed. + /// + protected virtual bool IsMemberAllowed(MemberInfo member) + { + return member is PropertyInfo; + } } diff --git a/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs new file mode 100644 index 0000000000..aba81af6f3 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs @@ -0,0 +1,29 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.TextTemplating.Razor; + +public class RazorTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase +{ + private readonly RazorTemplateRenderingEngine _engine; + + public RazorTemplateRenderingEngine_IsSandboxed_Tests() + { + _engine = GetRequiredService(); + } + + [Fact] + public void Razor_Engine_Should_Not_Be_Sandboxed() + { + // Razor templates compile into fully-trusted .NET code; editing them is + // equivalent to granting server-side code execution. + _engine.IsSandboxed.ShouldBeFalse(); + } + + [Fact] + public void Razor_Engine_Should_Expose_IsSandboxed_Through_Interface() + { + ITemplateRenderingEngine asInterface = _engine; + asInterface.IsSandboxed.ShouldBeFalse(); + } +} diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl new file mode 100644 index 0000000000..9666a9e986 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl @@ -0,0 +1 @@ +danger=[{{ model.dangerous_action }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl new file mode 100644 index 0000000000..14eb44cce2 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl @@ -0,0 +1 @@ +name=[{{ model.name }}] email=[{{ model.inner.email }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl new file mode 100644 index 0000000000..6ccfcfcef6 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl @@ -0,0 +1 @@ +getType=[{{ model.GetType }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl new file mode 100644 index 0000000000..ada402ac57 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl @@ -0,0 +1 @@ +loaded=[{{ model.GetType.Assembly.GetType "System.IO.File" }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs new file mode 100644 index 0000000000..2aa0cd018f --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs @@ -0,0 +1,123 @@ +using System.Threading.Tasks; +using global::Scriban.Syntax; +using Shouldly; +using Xunit; + +namespace Volo.Abp.TextTemplating.Scriban; + +public class ScribanTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase +{ + private readonly ScribanTemplateRenderingEngine _engine; + private readonly ITemplateRenderer _templateRenderer; + + public ScribanTemplateRenderingEngine_IsSandboxed_Tests() + { + _engine = GetRequiredService(); + _templateRenderer = GetRequiredService(); + } + + [Fact] + public void Scriban_Engine_Should_Be_Sandboxed() + { + // Scriban interprets templates as a restricted DSL without .NET interop; + // editing template content is safe for non-developer users. + _engine.IsSandboxed.ShouldBeTrue(); + } + + [Fact] + public void Scriban_Engine_Should_Expose_IsSandboxed_Through_Interface() + { + ITemplateRenderingEngine asInterface = _engine; + asInterface.IsSandboxed.ShouldBeTrue(); + } + + [Fact] + public async Task Should_Hide_GetType_Member_On_Imported_Model() + { + // The engine projects .NET model objects into a ScriptObject containing + // only the model's public readable properties. Reflection entry points + // such as object.GetType are not part of the projection, so + // "{{ model.GetType }}" silently resolves to null (empty output) instead + // of leaking the runtime type. + var result = await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.ReflectionEscapeAttempt, + model: new ReflectionEscapeModel { Name = "John" }); + + result.ShouldBe("getType=[]\n"); + result.ShouldNotContain("RuntimeType"); + result.ShouldNotContain("Volo.Abp.TextTemplating"); + } + + [Fact] + public async Task Should_Block_Reflection_Escape_Chain() + { + // Direct reflection chain that an attacker would attempt: + // {{ model.GetType.Assembly.GetType "System.IO.File" }} + // Because the projected ScriptObject does not expose GetType, the first + // hop yields null and Scriban raises a ScriptRuntimeException when the + // chain dereferences a null. Surfacing an error is desirable: silent + // failure could mask escape attempts. + var ex = await Should.ThrowAsync(async () => + await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.ReflectionEscapeChain, + model: new ReflectionEscapeModel { Name = "John" })); + + // The error must reference the broken chain (null), not a leaked Type + // or Assembly value. + ex.Message.ShouldContain("null"); + ex.Message.ShouldNotContain("System.IO.File"); + ex.Message.ShouldNotContain("System.Private.CoreLib"); + } + + [Fact] + public async Task Should_Hide_Methods_Of_Imported_Model() + { + // Methods on .NET objects are not exposed by the projection so + // attackers cannot invoke side-effecting methods (e.g. repository + // mutators) even when the host accidentally imports a service-like + // object as a model. + var result = await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.MethodInvocationAttempt, + model: new ServiceLikeModel()); + + result.ShouldBe("danger=[]\n"); + result.ShouldNotContain("UNSAFE"); + } + + [Fact] + public async Task Should_Project_Properties_Including_Nested_Objects() + { + // The projection must still expose user-defined readable properties + // (and recurse into nested objects) so legitimate template usage keeps + // working after sandboxing. + var result = await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.NestedPropertyAccess, + model: new OuterModel { Name = "John", Inner = new InnerModel { Email = "j@a.b" } }); + + result.ShouldBe("name=[John] email=[j@a.b]\n"); + } + + private class ReflectionEscapeModel + { + public string Name { get; set; } = default!; + } + + private class ServiceLikeModel + { + public string DangerousAction() + { + return "UNSAFE"; + } + } + + private class OuterModel + { + public string Name { get; set; } = default!; + public InnerModel Inner { get; set; } = default!; + } + + private class InnerModel + { + public string Email { get; set; } = default!; + } +} diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs index 0087288c32..006f2db450 100644 --- a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs @@ -2,6 +2,11 @@ public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider { + public const string ReflectionEscapeAttempt = "ReflectionEscapeAttempt"; + public const string ReflectionEscapeChain = "ReflectionEscapeChain"; + public const string MethodInvocationAttempt = "MethodInvocationAttempt"; + public const string NestedPropertyAccess = "NestedPropertyAccess"; + public override void Define(ITemplateDefinitionContext context) { context.GetOrNull(TestTemplates.WelcomeEmail)? @@ -19,5 +24,21 @@ public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider context.GetOrNull(TestTemplates.ShowDecimalNumber)? .WithVirtualFilePath("/SampleTemplates/ShowDecimalNumber.tpl", true) .WithScribanEngine(); + + context.Add(new TemplateDefinition(ReflectionEscapeAttempt) + .WithVirtualFilePath("/SampleTemplates/ReflectionEscapeAttempt.tpl", true) + .WithScribanEngine()); + + context.Add(new TemplateDefinition(ReflectionEscapeChain) + .WithVirtualFilePath("/SampleTemplates/ReflectionEscapeChain.tpl", true) + .WithScribanEngine()); + + context.Add(new TemplateDefinition(MethodInvocationAttempt) + .WithVirtualFilePath("/SampleTemplates/MethodInvocationAttempt.tpl", true) + .WithScribanEngine()); + + context.Add(new TemplateDefinition(NestedPropertyAccess) + .WithVirtualFilePath("/SampleTemplates/NestedPropertyAccess.tpl", true) + .WithScribanEngine()); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json index dc870f6878..8b8d9a19a5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json @@ -406,7 +406,7 @@ "uniqueName": "MoveAllBlogPostsAsyncByBlogIdAndAssignToBlogId", "name": "MoveAllBlogPostsAsync", "httpMethod": "PUT", - "url": "api/cms-kit-admin/blogs/{id}/move-all-blog-posts", + "url": "api/cms-kit-admin/blogs/{blogId}/move-all-blog-posts", "supportedVersions": [], "parametersOnMethod": [ { @@ -436,7 +436,7 @@ "isOptional": false, "defaultValue": null, "constraintTypes": null, - "bindingSourceId": "ModelBinding", + "bindingSourceId": "Path", "descriptorName": "" }, { @@ -450,18 +450,6 @@ "constraintTypes": null, "bindingSourceId": "Query", "descriptorName": "" - }, - { - "nameOnMethod": "id", - "name": "id", - "jsonName": null, - "type": null, - "typeSimple": null, - "isOptional": false, - "defaultValue": null, - "constraintTypes": [], - "bindingSourceId": "Path", - "descriptorName": "" } ], "returnValue": { diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs index f90215ed38..9d56b60231 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs @@ -71,7 +71,7 @@ public class BlogAdminController : CmsKitAdminController, IBlogAdminAppService } [HttpPut] - [Route("{id}/move-all-blog-posts")] + [Route("{blogId}/move-all-blog-posts")] [Authorize(CmsKitAdminPermissions.Blogs.Delete)] public Task MoveAllBlogPostsAsync(Guid blogId, [FromQuery]Guid? assignToBlogId) { diff --git a/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs b/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs index ea387b2794..9bd3c0aefd 100644 --- a/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs +++ b/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs @@ -9,5 +9,7 @@ namespace Volo.Docs.Documents public string DisplayName { get; set; } public Dictionary Values { get; set; } + + public Dictionary> DependsOn { get; set; } } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index 521ba70ffd..a68298a431 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -474,7 +474,12 @@
- @if (Model.DocumentPreferences != null && Model.DocumentPreferences.Parameters != null && Model.DocumentPreferences.Parameters.Any()) + @{ + var visibleParameters = Model.DocumentPreferences?.Parameters? + .Where(p => Model.IsParameterVisible(p)) + .ToList() ?? new List(); + } + @if (visibleParameters.Count > 0) {
@@ -488,10 +493,11 @@ @{ const int maxCellCount = 3; - var count = Model.DocumentPreferences.Parameters.Count; + var count = visibleParameters.Count; var rowCount = count / maxCellCount + (count % maxCellCount > 0 ? 1 : 0); var cellSize = 12 / (count > maxCellCount ? maxCellCount : count); - var latestCellSize = 12 / count - (rowCount - 1) * maxCellCount; + var lastRowCellCount = count - (rowCount - 1) * maxCellCount; + var latestCellSize = 12 / lastRowCellCount; string BuildParameterDivClass(int index) { @@ -503,7 +509,7 @@ for (var i = 0; i < count; ++i) { - var parameter = Model.DocumentPreferences.Parameters[i]; + var parameter = visibleParameters[i];
diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index 35e3c05f15..6eb3c02a8c 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -90,6 +90,29 @@ namespace Volo.Docs.Pages.Documents.Project public DocumentRenderParameters UserPreferences { get; set; } = new DocumentRenderParameters(); + private HashSet? _renderedParameterNamesCache; + private HashSet RenderedParameterNames => + _renderedParameterNamesCache ??= (DocumentPreferences?.Parameters?.Select(p => p.Name).ToHashSet() ?? new HashSet()); + + public virtual bool IsParameterVisible(DocumentParameterDto parameter) + { + return IsParameterVisibleGiven(parameter, UserPreferences); + } + + protected virtual bool IsParameterVisibleGiven(DocumentParameterDto parameter, IReadOnlyDictionary selectedValues) + { + if (parameter.DependsOn == null || parameter.DependsOn.Count == 0) + { + return true; + } + + return parameter.DependsOn.All(rule => + rule.Value == null || !RenderedParameterNames.Contains(rule.Key) + || (rule.Value.Count > 0 + && selectedValues.TryGetValue(rule.Key, out var current) + && rule.Value.Contains(current))); + } + public List AlternativeOptionLinkQueries { get; set; } = new List(); public bool FullSearchEnabled { get; set; } @@ -827,7 +850,8 @@ namespace Volo.Docs.Pages.Documents.Project { Name = parameter.Name, DisplayName = parameter.DisplayName, - Values = new Dictionary() + Values = new Dictionary(), + DependsOn = parameter.DependsOn }; foreach (var value in parameter.Values) @@ -847,13 +871,13 @@ namespace Volo.Docs.Pages.Documents.Project { if (!DocumentPreferences?.Parameters?.Any() ?? true) { - return; + return; } - AlternativeOptionLinkQueries = CollectAlternativeOptionLinksRecursively(); + AlternativeOptionLinkQueries = CollectAlternativeOptionLinksRecursively(0, new Dictionary()); } - private List CollectAlternativeOptionLinksRecursively(int index = 0) + private List CollectAlternativeOptionLinksRecursively(int index, Dictionary selected) { if (index >= DocumentPreferences.Parameters.Count) { @@ -861,13 +885,20 @@ namespace Volo.Docs.Pages.Documents.Project } var option = DocumentPreferences.Parameters[index]; + + if (!IsParameterVisibleGiven(option, selected)) + { + return CollectAlternativeOptionLinksRecursively(index + 1, selected); + } + var queries = new List(); foreach (var key in option.Values.Keys) { - var linkQuery = new StringBuilder($"{option.Name}={key}"); + var linkQuery = $"{option.Name}={key}"; - var restOfQueries = CollectAlternativeOptionLinksRecursively(index + 1); + var nextSelected = new Dictionary(selected) { [option.Name] = key }; + var restOfQueries = CollectAlternativeOptionLinksRecursively(index + 1, nextSelected); if (restOfQueries.Any()) { @@ -878,7 +909,7 @@ namespace Volo.Docs.Pages.Documents.Project } else { - queries.Add($"{linkQuery}"); + queries.Add(linkQuery); } }