Skip to content

Commit d37d0d3

Browse files
Add Moodle provider (#532)
* Added Moodle support * Changes applied according to the review * revert space changes * Added UnitTests * Update src/AspNet.Security.OAuth.Moodle/AspNet.Security.OAuth.Moodle.csproj Co-authored-by: Martin Costello <[email protected]> * Added UnitTests for Chinese * Update test/AspNet.Security.OAuth.Providers.Tests/Moodle/MoodleTests.cs Co-authored-by: Martin Costello <[email protected]> * swapped System.Diagnostics.CodeAnalysis to JetBrains.Annotations Co-authored-by: Martin Costello <[email protected]>
1 parent 4b83b0a commit d37d0d3

14 files changed

+695
-6
lines changed

AspNet.Security.OAuth.Providers.sln

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.AmoCr
208208
EndProject
209209
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Alipay", "src\AspNet.Security.OAuth.Alipay\AspNet.Security.OAuth.Alipay.csproj", "{61AB67B0-0F4A-47A2-A4D8-9738AA34C468}"
210210
EndProject
211-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Line", "src\AspNet.Security.OAuth.Line\AspNet.Security.OAuth.Line.csproj", "{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}"
211+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Line", "src\AspNet.Security.OAuth.Line\AspNet.Security.OAuth.Line.csproj", "{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}"
212+
EndProject
213+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Moodle", "src\AspNet.Security.OAuth.Moodle\AspNet.Security.OAuth.Moodle.csproj", "{1745B11F-F700-4604-B61F-54B962A6BE9A}"
212214
EndProject
213215
Global
214216
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -492,14 +494,18 @@ Global
492494
{DDF7546B-51C4-46EB-B102-7EE720E8E74F}.Debug|Any CPU.Build.0 = Debug|Any CPU
493495
{DDF7546B-51C4-46EB-B102-7EE720E8E74F}.Release|Any CPU.ActiveCfg = Release|Any CPU
494496
{DDF7546B-51C4-46EB-B102-7EE720E8E74F}.Release|Any CPU.Build.0 = Release|Any CPU
495-
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
496-
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
497-
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
498-
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Release|Any CPU.Build.0 = Release|Any CPU
499497
{61AB67B0-0F4A-47A2-A4D8-9738AA34C468}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
500498
{61AB67B0-0F4A-47A2-A4D8-9738AA34C468}.Debug|Any CPU.Build.0 = Debug|Any CPU
501499
{61AB67B0-0F4A-47A2-A4D8-9738AA34C468}.Release|Any CPU.ActiveCfg = Release|Any CPU
502500
{61AB67B0-0F4A-47A2-A4D8-9738AA34C468}.Release|Any CPU.Build.0 = Release|Any CPU
501+
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
502+
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
503+
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
504+
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B}.Release|Any CPU.Build.0 = Release|Any CPU
505+
{1745B11F-F700-4604-B61F-54B962A6BE9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
506+
{1745B11F-F700-4604-B61F-54B962A6BE9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
507+
{1745B11F-F700-4604-B61F-54B962A6BE9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
508+
{1745B11F-F700-4604-B61F-54B962A6BE9A}.Release|Any CPU.Build.0 = Release|Any CPU
503509
EndGlobalSection
504510
GlobalSection(SolutionProperties) = preSolution
505511
HideSolutionNode = FALSE
@@ -580,8 +586,9 @@ Global
580586
{1B19314C-9F33-4E71-AF0C-46ED8AB621CE} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
581587
{26DDE1D4-45D5-4CAA-ACB6-71FECF781B27} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
582588
{DDF7546B-51C4-46EB-B102-7EE720E8E74F} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
583-
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
584589
{61AB67B0-0F4A-47A2-A4D8-9738AA34C468} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
590+
{6A2FEBAB-2372-4043-BD37-B2E94887BA9B} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
591+
{1745B11F-F700-4604-B61F-54B962A6BE9A} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
585592
EndGlobalSection
586593
GlobalSection(ExtensibilityGlobals) = postSolution
587594
SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
150150
| LinkedIn | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.LinkedIn?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.LinkedIn/ "Download AspNet.Security.OAuth.LinkedIn from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.LinkedIn?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.LinkedIn "Download AspNet.Security.OAuth.LinkedIn from MyGet.org") | [Documentation](https://docs.microsoft.com/en-us/linkedin/shared/authentication/authentication "LinkedIn developer documentation") |
151151
| MailChimp | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.MailChimp?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.MailChimp/ "Download AspNet.Security.OAuth.MailChimp from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.MailChimp?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.MailChimp "Download AspNet.Security.OAuth.MailChimp from MyGet.org") | [Documentation](https://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-oauth2/ "MailChimp developer documentation") |
152152
| MailRu | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.MailRu?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.MailRu/ "Download AspNet.Security.OAuth.MailRu from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.MailRu?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.MailRu "Download AspNet.Security.OAuth.MailRu from MyGet.org") | [Documentation](https://o2.mail.ru/docs#web "MailRu developer documentation") |
153+
| Moodle | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Moodle?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Moodle/ "Download AspNet.Security.OAuth.Moodle from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Moodle?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Moodle "Download AspNet.Security.OAuth.Moodle from MyGet.org") | [Documentation](https://github.com/projectestac/moodle-local_oauth "Moodle OAuth2 plugin developer documentation") |
153154
| Myob | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Myob?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Myob/ "Download AspNet.Security.OAuth.Myob from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Myob?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Myob "Download AspNet.Security.OAuth.Myob from MyGet.org") | [Documentation](https://developer.myob.com/api/accountright/api-overview/authentication/ "Myob developer documentation") |
154155
| NetEase | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.NetEase?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.NetEase/ "Download AspNet.Security.OAuth.NetEase from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.NetEase?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.NetEase "Download AspNet.Security.OAuth.NetEase from MyGet.org") | [Documentation](https://reg.163.com/help/help_oauth2.html "NetEase developer documentation") |
155156
| Nextcloud | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Nextcloud?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Nextcloud/ "Download AspNet.Security.OAuth.Nextcloud from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Nextcloud?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Nextcloud "Download AspNet.Security.OAuth.Nextcloud from MyGet.org") | [Documentation](https://docs.nextcloud.com/server/14/admin_manual/configuration_server/oauth2.html "Nextcloud developer documentation") [User EndPoint Documentation](https://docs.nextcloud.com/server/15/developer_manual/client_apis/OCS/index.html#user-metadata "Nextcloud developer documentation") |

docs/moodle.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Integrating the Moodle Provider
2+
3+
Applies to the Moodle plugin [projectestac/moodle-local_oauth](https://github.com/projectestac/moodle-local_oauth).
4+
5+
## Example
6+
7+
```csharp
8+
services.AddAuthentication(options => /* Auth configuration */)
9+
.AddMoodle(options =>
10+
{
11+
options.ClientId = "my-client-id";
12+
options.ClientSecret = "my-client-secret";
13+
options.Domain = "mymoodlesite.com";
14+
});
15+
```
16+
17+
## Required Additional Settings
18+
19+
| Property Name | Property Type | Description | Default Value |
20+
|:--|:--|:--|:--|
21+
| `Domain` | `string?` | The Moodle domain (_Org URL_) of your site. For example: "mymoodlesite.com". | `null` |
22+
23+
## Optional Settings
24+
25+
_None._
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net5.0</TargetFrameworks>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<Description>ASP.NET Core security middleware enabling Moodle authentication.</Description>
9+
<Authors>HIT-ReFreSH</Authors>
10+
<PackageTags>aspnetcore;authentication;moodle;oauth;security</PackageTags>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
15+
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
namespace AspNet.Security.OAuth.Moodle
8+
{
9+
/// <summary>
10+
/// Contains constants specific to the <see cref="MoodleAuthenticationHandler"/>.
11+
/// </summary>
12+
public static class MoodleAuthenticationConstants
13+
{
14+
public static class Claims
15+
{
16+
public const string IdNumber = "urn:moodle:idnumber";
17+
public const string MoodleId = "urn:moodle:id";
18+
public const string Language = "urn:moodle:lang";
19+
}
20+
}
21+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using Microsoft.AspNetCore.Authentication;
8+
using Microsoft.AspNetCore.Authentication.OAuth;
9+
10+
namespace AspNet.Security.OAuth.Moodle
11+
{
12+
/// <summary>
13+
/// Default values used by the Moodle authentication middleware.
14+
/// </summary>
15+
public class MoodleAuthenticationDefaults
16+
{
17+
/// <summary>
18+
/// Default value for <see cref="AuthenticationScheme.Name"/>.
19+
/// </summary>
20+
public const string AuthenticationScheme = "Moodle";
21+
22+
/// <summary>
23+
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
24+
/// </summary>
25+
public static readonly string DisplayName = "Moodle";
26+
27+
/// <summary>
28+
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
29+
/// </summary>
30+
public const string Issuer = "Moodle";
31+
32+
/// <summary>
33+
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
34+
/// </summary>
35+
public const string CallbackPath = "/signin-moodle";
36+
37+
/// <summary>
38+
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
39+
/// </summary>
40+
public static readonly string AuthorizationEndpointPath = "/local/oauth/login.php";
41+
42+
/// <summary>
43+
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
44+
/// </summary>
45+
public static readonly string TokenEndpointPath = "/local/oauth/token.php";
46+
47+
/// <summary>
48+
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
49+
/// </summary>
50+
public static readonly string UserInformationEndpointPath = "/local/oauth/user_info.php";
51+
}
52+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System;
8+
using AspNet.Security.OAuth.Moodle;
9+
using JetBrains.Annotations;
10+
using Microsoft.AspNetCore.Authentication;
11+
using Microsoft.Extensions.DependencyInjection.Extensions;
12+
using Microsoft.Extensions.Options;
13+
14+
namespace Microsoft.Extensions.DependencyInjection
15+
{
16+
/// <summary>
17+
/// Extension methods to add Moodle authentication capabilities to an HTTP application pipeline.
18+
/// </summary>
19+
public static class MoodleAuthenticationExtensions
20+
{
21+
/// <summary>
22+
/// Adds <see cref="MoodleAuthenticationHandler"/> to the specified
23+
/// <see cref="AuthenticationBuilder"/>, which enables Moodle authentication capabilities.
24+
/// </summary>
25+
/// <param name="builder">The authentication builder.</param>
26+
/// <returns>A reference to this instance after the operation has completed.</returns>
27+
public static AuthenticationBuilder AddMoodle([NotNull] this AuthenticationBuilder builder)
28+
{
29+
return builder.AddMoodle(MoodleAuthenticationDefaults.AuthenticationScheme, _ => { });
30+
}
31+
32+
/// <summary>
33+
/// Adds <see cref="MoodleAuthenticationHandler"/> to the specified
34+
/// <see cref="AuthenticationBuilder"/>, which enables Moodle authentication capabilities.
35+
/// </summary>
36+
/// <param name="builder">The authentication builder.</param>
37+
/// <param name="configuration">The delegate used to configure the OpenID 2.0 options.</param>
38+
/// <returns>A reference to this instance after the operation has completed.</returns>
39+
public static AuthenticationBuilder AddMoodle(
40+
[NotNull] this AuthenticationBuilder builder,
41+
[NotNull] Action<MoodleAuthenticationOptions> configuration)
42+
{
43+
return builder.AddMoodle(MoodleAuthenticationDefaults.AuthenticationScheme, configuration);
44+
}
45+
46+
/// <summary>
47+
/// Adds <see cref="MoodleAuthenticationHandler"/> to the specified
48+
/// <see cref="AuthenticationBuilder"/>, which enables Moodle authentication capabilities.
49+
/// </summary>
50+
/// <param name="builder">The authentication builder.</param>
51+
/// <param name="scheme">The authentication scheme associated with this instance.</param>
52+
/// <param name="configuration">The delegate used to configure the Moodle options.</param>
53+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
54+
public static AuthenticationBuilder AddMoodle(
55+
[NotNull] this AuthenticationBuilder builder,
56+
[NotNull] string scheme,
57+
[NotNull] Action<MoodleAuthenticationOptions> configuration)
58+
{
59+
return builder.AddMoodle(scheme, MoodleAuthenticationDefaults.DisplayName, configuration);
60+
}
61+
62+
/// <summary>
63+
/// Adds <see cref="MoodleAuthenticationHandler"/> to the specified
64+
/// <see cref="AuthenticationBuilder"/>, which enables Moodle authentication capabilities.
65+
/// </summary>
66+
/// <param name="builder">The authentication builder.</param>
67+
/// <param name="scheme">The authentication scheme associated with this instance.</param>
68+
/// <param name="caption">The optional display name associated with this instance.</param>
69+
/// <param name="configuration">The delegate used to configure the Moodle options.</param>
70+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
71+
public static AuthenticationBuilder AddMoodle(
72+
[NotNull] this AuthenticationBuilder builder,
73+
[NotNull] string scheme,
74+
[NotNull] string caption,
75+
[NotNull] Action<MoodleAuthenticationOptions> configuration)
76+
{
77+
builder.Services.TryAddSingleton<IPostConfigureOptions<MoodleAuthenticationOptions>, MoodlePostConfigureOptions>();
78+
return builder.AddOAuth<MoodleAuthenticationOptions, MoodleAuthenticationHandler>(scheme, caption, configuration);
79+
}
80+
}
81+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System.Net.Http;
8+
using System.Net.Http.Headers;
9+
using System.Security.Claims;
10+
using System.Text.Encodings.Web;
11+
using System.Text.Json;
12+
using System.Threading.Tasks;
13+
using JetBrains.Annotations;
14+
using Microsoft.AspNetCore.Authentication;
15+
using Microsoft.AspNetCore.Authentication.OAuth;
16+
using Microsoft.Extensions.Logging;
17+
using Microsoft.Extensions.Options;
18+
19+
namespace AspNet.Security.OAuth.Moodle
20+
{
21+
public class MoodleAuthenticationHandler : OAuthHandler<MoodleAuthenticationOptions>
22+
{
23+
public MoodleAuthenticationHandler(
24+
[NotNull] IOptionsMonitor<MoodleAuthenticationOptions> options,
25+
[NotNull] ILoggerFactory logger,
26+
[NotNull] UrlEncoder encoder,
27+
[NotNull] ISystemClock clock)
28+
: base(options, logger, encoder, clock)
29+
{
30+
}
31+
32+
protected override async Task<AuthenticationTicket> CreateTicketAsync(
33+
[NotNull] ClaimsIdentity identity,
34+
[NotNull] AuthenticationProperties properties,
35+
[NotNull] OAuthTokenResponse tokens)
36+
{
37+
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
38+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
39+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
40+
41+
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
42+
if (!response.IsSuccessStatusCode)
43+
{
44+
Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
45+
"returned a {Status} response with the following payload: {Headers} {Body}.",
46+
/* Status: */ response.StatusCode,
47+
/* Headers: */ response.Headers.ToString(),
48+
/* Body: */ await response.Content.ReadAsStringAsync(Context.RequestAborted));
49+
50+
throw new HttpRequestException("An error occurred while retrieving the user profile.");
51+
}
52+
53+
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
54+
55+
var principal = new ClaimsPrincipal(identity);
56+
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
57+
context.RunClaimActions();
58+
59+
await Options.Events.CreatingTicket(context);
60+
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)