зеркало из https://github.com/dotnet/aspnetcore.git
[Blazor] Enable regex constraint in Blazor routing (#53533)
This was discovered when working on the issue for #53138. The regex constraint is not enabled by default on the blazor router. As a result if a route uses a regex constraint, it will work on SSR but will fail the moment the Blazor router tries to construct the route. The fix enables the regex constraint on the blazor router, unconditionally for server, and behind a feature flag for webassembly. This is because enabling the regex constraint adds +80kb to the payload due to the inclusion of the System.Text.RegularExpressions assembly.
This commit is contained in:
Родитель
ca988856b7
Коммит
f51ae60426
|
@ -11405,6 +11405,7 @@ Global
|
||||||
{9788C76F-658B-4441-88F8-22C6B86FAD27} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
|
{9788C76F-658B-4441-88F8-22C6B86FAD27} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
|
||||||
{1970D5CD-D9A4-4673-A297-179BB04199F4} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
|
{1970D5CD-D9A4-4673-A297-179BB04199F4} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
|
||||||
{A40350FE-4334-4007-B1C3-6BEB1B070309} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
|
{A40350FE-4334-4007-B1C3-6BEB1B070309} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
|
||||||
|
{A40350FE-4334-4007-B1C3-6BEB1B070308} = {6126DCE4-9692-4EE2-B240-C65743572995}
|
||||||
{C1E7F837-6988-43E2-9E1C-7302DB484F99} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
|
{C1E7F837-6988-43E2-9E1C-7302DB484F99} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
|
||||||
{2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
|
{2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
|
||||||
{7CB09412-C9B0-47E8-A8C3-311AA4CFDE04} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
|
{7CB09412-C9B0-47E8-A8C3-311AA4CFDE04} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
<Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" />
|
<Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" />
|
||||||
|
|
||||||
<Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" />
|
<Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" />
|
||||||
<Compile Remove="$(RoutingSourceRoot)Constraints\RegexErrorStubRouteConstraint.cs" />
|
|
||||||
<Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" />
|
<Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" />
|
||||||
<Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" />
|
<Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -3,5 +3,8 @@
|
||||||
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadManager" feature="System.Reflection.Metadata.MetadataUpdater.IsSupported" featurevalue="false">
|
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadManager" feature="System.Reflection.Metadata.MetadataUpdater.IsSupported" featurevalue="false">
|
||||||
<method signature="System.Boolean get_MetadataUpdateSupported()" body="stub" value="false" />
|
<method signature="System.Boolean get_MetadataUpdateSupported()" body="stub" value="false" />
|
||||||
</type>
|
</type>
|
||||||
|
<type fullname="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport" feature="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport" featurevalue="false">
|
||||||
|
<method signature="System.Boolean get_IsEnabled()" body="stub" value="false" />
|
||||||
|
</type>
|
||||||
</assembly>
|
</assembly>
|
||||||
</linker>
|
</linker>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Routing;
|
||||||
|
|
||||||
|
internal static class RegexConstraintSupport
|
||||||
|
{
|
||||||
|
// We should check the AppContext switch in the implementation, but it doesn't flow to the wasm runtime
|
||||||
|
// during development, so we can't offer a better experience (build time message to enable the switch)
|
||||||
|
// until the context switch flows to the runtime.
|
||||||
|
// This value gets updated by the linker when the app is trimmed, so the code will always be removed from
|
||||||
|
// webassembly unless the switch is enabled.
|
||||||
|
public static bool IsEnabled =>
|
||||||
|
AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", out var enabled) && enabled;
|
||||||
|
}
|
|
@ -154,7 +154,7 @@
|
||||||
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
|
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve">
|
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve">
|
||||||
<value>A route parameter uses the regex constraint, which isn't registered. If this application was configured using CreateSlimBuilder(...) or AddRoutingCore(...) then this constraint is not registered by default. To use the regex constraint, configure route options at app startup: services.Configure<RouteOptions>(options => options.SetParameterPolicy<RegexInlineRouteConstraint>("regex"));</value>
|
<value>A route parameter uses the regex constraint, which isn't registered. To enable it add the property 'BlazorRoutingEnableRegexConstraint' to your project file inside a `PropertyGroup`.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
|
<data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
|
||||||
<value>Value must be greater than or equal to {0}.</value>
|
<value>Value must be greater than or equal to {0}.</value>
|
||||||
|
|
|
@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using static Microsoft.AspNetCore.Internal.LinkerFlags;
|
using static Microsoft.AspNetCore.Internal.LinkerFlags;
|
||||||
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components;
|
namespace Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
@ -112,9 +113,14 @@ internal class RouteTableFactory
|
||||||
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
|
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
|
||||||
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider)
|
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
|
var routeOptions = Options.Create(new RouteOptions());
|
||||||
|
if (!OperatingSystem.IsBrowser() || RegexConstraintSupport.IsEnabled)
|
||||||
|
{
|
||||||
|
routeOptions.Value.SetParameterPolicy("regex", typeof(RegexInlineRouteConstraint));
|
||||||
|
}
|
||||||
var builder = new TreeRouteBuilder(
|
var builder = new TreeRouteBuilder(
|
||||||
serviceProvider.GetRequiredService<ILoggerFactory>(),
|
serviceProvider.GetRequiredService<ILoggerFactory>(),
|
||||||
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), serviceProvider));
|
new DefaultInlineConstraintResolver(routeOptions, serviceProvider));
|
||||||
|
|
||||||
foreach (var (type, templates) in templatesByHandler)
|
foreach (var (type, templates) in templatesByHandler)
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
using Microsoft.AspNetCore.Components.Infrastructure;
|
using Microsoft.AspNetCore.Components.Infrastructure;
|
||||||
|
@ -76,6 +77,12 @@ public sealed class WebAssemblyHostBuilder
|
||||||
Services = new ServiceCollection();
|
Services = new ServiceCollection();
|
||||||
Logging = new LoggingBuilder(Services);
|
Logging = new LoggingBuilder(Services);
|
||||||
|
|
||||||
|
var assembly = Assembly.GetEntryAssembly();
|
||||||
|
if (assembly != null)
|
||||||
|
{
|
||||||
|
InitializeRoutingAppContextSwitch(assembly);
|
||||||
|
}
|
||||||
|
|
||||||
InitializeWebAssemblyRenderer();
|
InitializeWebAssemblyRenderer();
|
||||||
|
|
||||||
// Retrieve required attributes from JSRuntimeInvoker
|
// Retrieve required attributes from JSRuntimeInvoker
|
||||||
|
@ -93,6 +100,22 @@ public sealed class WebAssemblyHostBuilder
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void InitializeRoutingAppContextSwitch(Assembly assembly)
|
||||||
|
{
|
||||||
|
var assemblyMetadataAttributes = assembly.GetCustomAttributes<AssemblyMetadataAttribute>();
|
||||||
|
foreach (var ama in assemblyMetadataAttributes)
|
||||||
|
{
|
||||||
|
if (string.Equals(ama.Key, "Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (ama.Value != null && string.Equals((string?)ama.Value, "true", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
AppContext.SetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")]
|
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")]
|
||||||
private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods)
|
private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
|
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
|
||||||
|
<BlazorRoutingEnableRegexConstraint Condition="'$(BlazorRoutingEnableRegexConstraint)' == ''">false</BlazorRoutingEnableRegexConstraint>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<RuntimeHostConfigurationOption Include="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport"
|
||||||
|
Condition="'$(BlazorRoutingEnableRegexConstraint)' != ''"
|
||||||
|
Value="$(BlazorRoutingEnableRegexConstraint)"
|
||||||
|
Trim="true" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(BlazorRoutingEnableRegexConstraint)' == 'true'">
|
||||||
|
<_Parameter1>Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport</_Parameter1>
|
||||||
|
<_Parameter2>true</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -39,10 +39,12 @@ public class UnifiedRoutingTests : ServerTestBase<BasicTestAppServerSiteFixture<
|
||||||
ExecuteRoutingTestCore(url, expectedValue);
|
ExecuteRoutingTestCore(url, expectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive()
|
[InlineData("routing/constraints/5", "5")]
|
||||||
|
[InlineData("%F0%9F%99%82/routing/constraints/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback", "http://www.example.com/login/callback")]
|
||||||
|
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive(string url, string expectedValue)
|
||||||
{
|
{
|
||||||
ExecuteRoutingTestCore("routing/constraints/5", "5");
|
ExecuteRoutingTestCore(url, expectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
@ -73,10 +75,12 @@ public class UnifiedRoutingTests : ServerTestBase<BasicTestAppServerSiteFixture<
|
||||||
ExecuteRoutingTestCore(url, expectedValue);
|
ExecuteRoutingTestCore(url, expectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive()
|
[InlineData("routing/constrained-catch-all/a/b", "a/b")]
|
||||||
|
[InlineData("%F0%9F%99%82/routing/constrained-catch-all/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback/another", "http://www.example.com/login/callback/another")]
|
||||||
|
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive(string url, string expectedValue)
|
||||||
{
|
{
|
||||||
ExecuteRoutingTestCore("routing/constrained-catch-all/a/b", "a/b");
|
ExecuteRoutingTestCore(url, expectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteRoutingTestCore(string url, string expectedValue)
|
private void ExecuteRoutingTestCore(string url, string expectedValue)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<Import Project="..\..\..\WebAssembly\WebAssembly\src\targets\Microsoft.AspNetCore.Components.WebAssembly.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
|
||||||
|
@ -7,6 +9,7 @@
|
||||||
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||||
<Nullable>annotations</Nullable>
|
<Nullable>annotations</Nullable>
|
||||||
<RazorLangVersion>latest</RazorLangVersion>
|
<RazorLangVersion>latest</RazorLangVersion>
|
||||||
|
<BlazorRoutingEnableRegexConstraint>true</BlazorRoutingEnableRegexConstraint>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
@page "/🙂/routing/constrained-catch-all/{*parameter:regex(http:%2F%2Fwww\\.example\\.com%2Flogin%2Fcallback\\/another)}"
|
||||||
|
<h3>Parameters</h3>
|
||||||
|
|
||||||
|
<p id="parameter-value">@Parameter</p>
|
||||||
|
|
||||||
|
@if (_interactive)
|
||||||
|
{
|
||||||
|
<p id="interactive">Rendered interactive.</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _interactive;
|
||||||
|
|
||||||
|
[Parameter] public string Parameter { get; set; }
|
||||||
|
|
||||||
|
protected override void OnAfterRender(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
_interactive = true;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
@page "/🙂/routing/constraints/{parameter:regex(http:%2F%2Fwww\\.example\\.com%2Flogin%2Fcallback)}"
|
||||||
|
<h3>Constrained Parameters</h3>
|
||||||
|
|
||||||
|
<p id="parameter-value">@Parameter</p>
|
||||||
|
|
||||||
|
@if (_interactive)
|
||||||
|
{
|
||||||
|
<p id="interactive">Rendered interactive.</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _interactive;
|
||||||
|
|
||||||
|
[Parameter] public string Parameter { get; set; }
|
||||||
|
|
||||||
|
protected override void OnAfterRender(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
_interactive = true;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,30 @@
|
||||||
// Licensed to the .NET Foundation under one or more agreements.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
#if !COMPONENTS
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
#else
|
||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Constraints;
|
namespace Microsoft.AspNetCore.Routing.Constraints;
|
||||||
|
|
||||||
|
#if !COMPONENTS
|
||||||
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
|
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
|
||||||
|
#else
|
||||||
|
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint, IParameterPolicy
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
public RegexErrorStubRouteConstraint(string _)
|
public RegexErrorStubRouteConstraint(string _)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
|
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !COMPONENTS
|
||||||
bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
|
bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
|
||||||
|
#else
|
||||||
|
bool IRouteConstraint.Match(string routeKey, RouteValueDictionary values)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
// Should never get called, but is same as throw in constructor in case constructor is changed.
|
// Should never get called, but is same as throw in constructor in case constructor is changed.
|
||||||
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
|
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
#if !COMPONENTS
|
#if !COMPONENTS
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
#else
|
||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
#endif
|
#endif
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Routing.Constraints;
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
|
@ -126,8 +128,13 @@ internal class RouteOptions
|
||||||
|
|
||||||
#if !COMPONENTS
|
#if !COMPONENTS
|
||||||
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
|
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
|
||||||
|
|
||||||
AddConstraint<RequiredRouteConstraint>(defaults, "required");
|
AddConstraint<RequiredRouteConstraint>(defaults, "required");
|
||||||
|
#else
|
||||||
|
// Check if the feature is not enabled in the browser context
|
||||||
|
if (OperatingSystem.IsBrowser() && !RegexConstraintSupport.IsEnabled)
|
||||||
|
{
|
||||||
|
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Files
|
// Files
|
||||||
|
|
Загрузка…
Ссылка в новой задаче