diff --git a/Parse.Tests/ConfigTests.cs b/Parse.Tests/ConfigTests.cs index a6c45f3c..191f8b65 100644 --- a/Parse.Tests/ConfigTests.cs +++ b/Parse.Tests/ConfigTests.cs @@ -61,7 +61,7 @@ public void TestCurrentConfig() public void TestToJSON() { IDictionary expectedJson = new Dictionary { { "params", new Dictionary { { "testKey", "testValue" } } } }; - Assert.AreEqual(JsonConvert.SerializeObject((Client.GetCurrentConfiguration() as IJsonConvertible).ConvertToJSON()), JsonConvert.SerializeObject(expectedJson)); + Assert.AreEqual(JsonConvert.SerializeObject((Client.GetCurrentConfiguration() as IJsonConvertible).ConvertToJson()), JsonConvert.SerializeObject(expectedJson)); } [TestMethod] diff --git a/Parse.Tests/UserTests.cs b/Parse.Tests/UserTests.cs index 4e205ff2..4795647e 100644 --- a/Parse.Tests/UserTests.cs +++ b/Parse.Tests/UserTests.cs @@ -602,7 +602,7 @@ public Task TestLink() hub.ObjectController = mockObjectController.Object; hub.CurrentUserController = new Mock { }.Object; - return user.LinkWithAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => + return user.LinkToServiceAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => { Assert.IsFalse(task.IsFaulted); Assert.IsFalse(task.IsCanceled); @@ -656,7 +656,7 @@ public Task TestUnlink() hub.ObjectController = mockObjectController.Object; hub.CurrentUserController = mockCurrentUserController.Object; - return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(task => + return user.UnlinkFromServiceAsync("parse", CancellationToken.None).ContinueWith(task => { Assert.IsFalse(task.IsFaulted); Assert.IsFalse(task.IsCanceled); @@ -710,7 +710,7 @@ public Task TestUnlinkNonCurrentUser() hub.ObjectController = mockObjectController.Object; hub.CurrentUserController = mockCurrentUserController.Object; - return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(task => + return user.UnlinkFromServiceAsync("parse", CancellationToken.None).ContinueWith(task => { Assert.IsFalse(task.IsFaulted); Assert.IsFalse(task.IsCanceled); @@ -747,7 +747,7 @@ public Task TestLogInWith() hub.UserController = mockController.Object; - return client.LogInWithAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => + return client.AuthenticateWithServiceAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => { Assert.IsFalse(task.IsFaulted); Assert.IsFalse(task.IsCanceled); diff --git a/Parse/Abstractions/Infrastructure/IJsonConvertible.cs b/Parse/Abstractions/Infrastructure/IJsonConvertible.cs index 5139a514..65013369 100644 --- a/Parse/Abstractions/Infrastructure/IJsonConvertible.cs +++ b/Parse/Abstractions/Infrastructure/IJsonConvertible.cs @@ -11,6 +11,6 @@ public interface IJsonConvertible /// Converts the object to a data structure that can be converted to JSON. /// /// An object to be JSONified. - IDictionary ConvertToJSON(); + IDictionary ConvertToJson(); } } diff --git a/Parse/Abstractions/Infrastructure/IServiceHubBuilder.cs b/Parse/Abstractions/Infrastructure/IServiceHubBuilder.cs new file mode 100644 index 00000000..23ce1d76 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IServiceHubBuilder.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Parse.Abstractions.Infrastructure +{ + // ALTERNATE NAME: IServiceHubRebuilder, IServiceHubConstructor, IServiceHubBuilder, IServiceHubStacker, ISericeHubCreator, IClient, IDataContainmentHub, IResourceContainmentHub, IDataContainer, IServiceHubComposer + + public interface IServiceHubBuilder + { + public IServiceHub BuildHub(IMutableServiceHub? serviceHub = default, IServiceHub? extension = default, params IServiceHubMutator[] mutators); + } +} diff --git a/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs b/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs index d88dea9e..dd7c5401 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs @@ -1,7 +1,17 @@ namespace Parse.Abstractions.Infrastructure { + /// + /// Generates an as a clone of another. This clone could be mutated. + /// public interface IServiceHubCloner { - public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators); + /// + /// Clones , via the if needed, and should execute the . The clone could be observed in a mutated state versus . + /// + /// The hub to use as a reference or combination extension to new hub. + /// The builder which could be used to create the clone. + /// Additional mutators to execute on the cloned hub. + /// A service hub which could be both based on , and observed in a mutated state versus . + public IServiceHub CloneHub(in IServiceHub hub, IServiceHubBuilder builder, params IServiceHubMutator[] mutators); } } diff --git a/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs b/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs deleted file mode 100644 index 73dc3597..00000000 --- a/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Parse.Abstractions.Infrastructure -{ - // ALTERNATE NAME: IClient, IDataContainmentHub, IResourceContainmentHub, IDataContainer, IServiceHubComposer - - public interface IServiceHubComposer - { - public IServiceHub BuildHub(IMutableServiceHub serviceHub = default, IServiceHub extension = default, params IServiceHubMutator[] configurators); - } -} diff --git a/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs b/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs index 6ce77d67..acd7f5a5 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs @@ -1,9 +1,11 @@ +using System.Collections.Generic; + namespace Parse.Abstractions.Infrastructure { // IServiceHubComposer, IServiceHubMutator, IServiceHubConfigurator, IClientConfigurator, IServiceConfigurationLayer /// - /// A class which makes a deliberate mutation to a service. + /// A definition of a deliberate mutation to a service hub. /// public interface IServiceHubMutator { @@ -12,11 +14,17 @@ public interface IServiceHubMutator /// bool Valid { get; } + //IServiceHubMutator[] Mutators => new[] { this }; + + // customHub, mutableHub, slice, target + // combinedHub, stackedHub, overallHub, resultantHub, composedHub + /// /// A method which mutates an implementation instance. /// - /// The target implementation instance - /// A hub which the is composed onto that should be used when needs to access services. - void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub); + /// The target implementation instance + /// A hub which the is combined onto that should be used when needs to access services. + /// The mutators that will be executed after this one. + void Mutate(ref IMutableServiceHub mutableHub, in IServiceHub consumableHub, Stack futureMutators); } } diff --git a/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs b/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs index c200086f..7a3fd9bf 100644 --- a/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs +++ b/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs @@ -4,6 +4,9 @@ namespace Parse.Abstractions.Platform.Authentication { + /// + /// An authenticator service for the Parse SDK. + /// public interface IParseAuthenticationProvider { /// @@ -33,6 +36,6 @@ public interface IParseAuthenticationProvider /// Provides a unique name for the type of authentication the provider does. /// For example, the FacebookAuthenticationProvider would return "facebook". /// - string AuthType { get; } + string Name { get; } } } diff --git a/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs b/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs index 416aad1a..7e1e2ae3 100644 --- a/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs +++ b/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Parse.Abstractions.Infrastructure; namespace Parse.Infrastructure @@ -12,17 +14,11 @@ public class AbsoluteCacheLocationMutator : IServiceHubMutator /// public string CustomAbsoluteCacheFilePath { get; set; } - /// /// - /// public bool Valid => CustomAbsoluteCacheFilePath is { }; - /// /// - /// - /// - /// - public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub, Stack futureMutators) { if ((target as IServiceHub).CacheController is IDiskFileCacheController { } diskFileCacheController) { diff --git a/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs b/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs index 44eb69ee..67542ff2 100644 --- a/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs +++ b/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs @@ -1,19 +1,33 @@ +using System.Collections.Generic; using System.Linq; using Parse.Abstractions.Infrastructure; using Parse.Platform.Users; namespace Parse.Infrastructure { + /// + /// Makes it so that clones can be made each with a reset , but otherwise identical services unless modified. + /// public class ConcurrentUserServiceHubCloner : IServiceHubCloner, IServiceHubMutator { + /// public bool Valid { get; } = true; - public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators) => composer.BuildHub(default, reference, requestedMutators.Concat(new[] { this }).ToArray()); + /// + /// Mutators which need to be executed on each new hub for a user. + /// + public List BoundMutators { get; set; } = new List { }; - public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + /// + public IServiceHub CloneHub(in IServiceHub hub, IServiceHubBuilder builder, params IServiceHubMutator[] mutators) => builder.BuildHub(default, hub, mutators.Concat(new[] { this }).ToArray()); + + /// + public void Mutate(ref IMutableServiceHub mutableHub, in IServiceHub consumableHub, Stack futureMutators) { - target.Cloner = this; - target.CurrentUserController = new ParseCurrentUserController(new TransientCacheController { }, composedHub.ClassController, composedHub.Decoder); + mutableHub.Cloner = this; + mutableHub.CurrentUserController = new ParseCurrentUserController(new TransientCacheController { }, consumableHub.ClassController, consumableHub.Decoder); + + BoundMutators.ForEach(mutator => futureMutators.Push(mutator)); } } } diff --git a/Parse/Infrastructure/Data/ParseDataEncoder.cs b/Parse/Infrastructure/Data/ParseDataEncoder.cs index 7fe99407..ee8874a4 100644 --- a/Parse/Infrastructure/Data/ParseDataEncoder.cs +++ b/Parse/Infrastructure/Data/ParseDataEncoder.cs @@ -32,7 +32,7 @@ public abstract class ParseDataEncoder ["base64"] = Convert.ToBase64String(bytes) }, ParseObject { } entity => EncodeObject(entity), - IJsonConvertible { } jsonConvertible => jsonConvertible.ConvertToJSON(), + IJsonConvertible { } jsonConvertible => jsonConvertible.ConvertToJson(), { } when Conversion.As>(value) is { } dictionary => dictionary.ToDictionary(pair => pair.Key, pair => Encode(pair.Value, serviceHub)), { } when Conversion.As>(value) is { } list => EncodeList(list, serviceHub), diff --git a/Parse/Infrastructure/FacebookAuthenticationMutator.cs b/Parse/Infrastructure/FacebookAuthenticationMutator.cs new file mode 100644 index 00000000..705f9001 --- /dev/null +++ b/Parse/Infrastructure/FacebookAuthenticationMutator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure +{ + /// + /// Enables Facebook authentication on the service hub. + /// + public class FacebookAuthenticationMutator : IServiceHubMutator + { + /// + public bool Valid => Caller is { Length: > 0 }; + + /// + /// The identifier to make API calls to Facebook with. This is in the Facebook Developer Dashboard as some variant of "Application ID" or "API Key". + /// + public string Caller { get; set; } + + /// + public void Mutate(ref IMutableServiceHub mutableHub, in IServiceHub consumableHub, Stack futureMutators) => mutableHub.InitializeFacebookAuthenticationProvider(Caller); + } +} diff --git a/Parse/Infrastructure/MetadataMutator.cs b/Parse/Infrastructure/MetadataMutator.cs index 351f779b..36fd506d 100644 --- a/Parse/Infrastructure/MetadataMutator.cs +++ b/Parse/Infrastructure/MetadataMutator.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Parse.Abstractions.Infrastructure; namespace Parse.Infrastructure @@ -13,10 +15,11 @@ public class MetadataMutator : MetadataController, IServiceHubMutator public bool Valid => this is { EnvironmentData: { OSVersion: { }, Platform: { }, TimeZone: { } }, HostManifestData: { Identifier: { }, Name: { }, ShortVersion: { }, Version: { } } }; /// - /// Sets the to the instance. + /// Sets the 's to the instance. /// - /// The to compose the information onto. - /// Thhe to use if a default service instance is required. - public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) => target.MetadataController = this; + /// The to compose the information onto. + /// The to use if a default service instance is required. + /// The mutators that will be executed after this one. + public void Mutate(ref IMutableServiceHub mutableHub, in IServiceHub consumableHub, Stack futureMutators) => mutableHub.MetadataController = this; } } diff --git a/Parse/Infrastructure/RelativeCacheLocationMutator.cs b/Parse/Infrastructure/RelativeCacheLocationMutator.cs index fe332424..93e4090e 100644 --- a/Parse/Infrastructure/RelativeCacheLocationMutator.cs +++ b/Parse/Infrastructure/RelativeCacheLocationMutator.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Parse.Abstractions.Infrastructure; namespace Parse.Infrastructure @@ -12,17 +14,11 @@ public class RelativeCacheLocationMutator : IServiceHubMutator /// public IRelativeCacheLocationGenerator RelativeCacheLocationGenerator { get; set; } - /// /// - /// public bool Valid => RelativeCacheLocationGenerator is { }; - /// /// - /// - /// - /// - public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) => target.CacheController = (target as IServiceHub).CacheController switch + public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub, Stack futureMutators) => target.CacheController = (target as IServiceHub).CacheController switch { null => new CacheController { RelativeCacheFilePath = RelativeCacheLocationGenerator.GetRelativeCacheFilePath(referenceHub) }, IDiskFileCacheController { } controller => (Controller: controller, controller.RelativeCacheFilePath = RelativeCacheLocationGenerator.GetRelativeCacheFilePath(referenceHub)).Controller, diff --git a/Parse/Infrastructure/Utilities/FacebookAuthenticationUtilities.cs b/Parse/Infrastructure/Utilities/FacebookAuthenticationUtilities.cs new file mode 100644 index 00000000..6cfb7a83 --- /dev/null +++ b/Parse/Infrastructure/Utilities/FacebookAuthenticationUtilities.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Authentication; + +namespace Parse.Infrastructure.Utilities +{ + // TODO: Add convenience methods for individual environments back. + + /// + /// Provides a set of utilities for the use of Facebook as an authenticator for Parse. + /// + public static partial class FacebookAuthenticationUtilities + { + static FacebookAuthenticationProvider Provider { get; set; } = new FacebookAuthenticationProvider { }; + + /// + /// Gets the Facebook Application ID as supplied to . + /// + public static string Identifier => Provider.Caller; + + /// + /// Gets the access token for the currently logged in Facebook user. This can be used with a + /// Facebook SDK to get access to Facebook user data. + /// + public static string AccessToken => Provider.AccessToken; + + /// + /// Initializes Facebook for use with Parse. + /// + /// The service hub to use. + /// Your Facebook application ID. + public static void InitializeFacebookAuthenticationProvider(this IServiceHub serviceHub, string identifier) + { + Provider.Caller = identifier; + serviceHub.SetAuthenticationProvider(Provider); + } + + /// + /// Initializes Facebook for use with Parse. + /// + /// The service hub to use. + /// The instance to use for Facebook authentication. + public static void InitializeFacebookAuthentication(this IServiceHub serviceHub, FacebookAuthenticationProvider authenticator) + { + if (authenticator is IServiceHubMutator { Valid: true }) + { + serviceHub.SetAuthenticationProvider(Provider = authenticator); + } + } + + /// + /// Logs in a using Facebook for authentication. If a user for the + /// given Facebook credentials does not already exist, a new user will be created. + /// + /// The service hub to use. + /// The user's Facebook ID. + /// A valid access token for the user. + /// The expiration date of the access token. + /// The cancellation token. + /// The user that was either logged in or created. + public static Task AuthenticateWithFacebookAsync(this IServiceHub serviceHub, string facebookId, string accessToken, DateTime expiration, CancellationToken cancellationToken = default) => serviceHub.AuthenticateWithServiceAsync("facebook", Provider.GetAuthenticationData(facebookId, accessToken, expiration), cancellationToken); + + /// + /// Links a to a Facebook account, allowing you to use Facebook + /// for authentication, and providing access to Facebook data for the user. + /// + /// The user to link to a Facebook account. + /// The user's Facebook ID. + /// A valid access token for the user. + /// The expiration date of the access token. + /// The cancellation token. + public static Task LinkToFacebookAsync(this ParseUser user, string facebookId, string accessToken, DateTime expiration, CancellationToken cancellationToken = default) => user.LinkToServiceAsync("facebook", Provider.GetAuthenticationData(facebookId, accessToken, expiration), cancellationToken); + + /// + /// Gets whether the given user is linked to a Facebook account. This can only be used on + /// the currently authorized user. + /// + /// The user to check. + /// true if the user is linked to a Facebook account. + public static bool CheckLinkedToFacebook(this ParseUser user) => user.CheckLinkedToService("facebook"); + + /// + /// Unlinks a user from a Facebook account. Unlinking a user will save the user's data. + /// + /// The user to unlink. + /// The cancellation token. + public static Task UnlinkFromFacebookAsync(this ParseUser user, CancellationToken cancellationToken = default) => user.UnlinkFromServiceAsync("facebook", cancellationToken); + } +} diff --git a/Parse/Infrastructure/Utilities/JsonUtilities.cs b/Parse/Infrastructure/Utilities/JsonUtilities.cs index 585e2110..17f2ebab 100644 --- a/Parse/Infrastructure/Utilities/JsonUtilities.cs +++ b/Parse/Infrastructure/Utilities/JsonUtilities.cs @@ -11,34 +11,49 @@ namespace Parse.Infrastructure.Utilities /// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org /// and http://tools.ietf.org/html/rfc4627 /// - public class JsonUtilities + public static class JsonUtilities { + internal static IDictionary DeserializeJsonText(string text) => Parse(text) as IDictionary; + + internal static string SerializeToJsonText(IDictionary data) => Encode(data); + /// /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. /// anchored at the index of the first character of the search, even when that search starts /// in the middle of the string). /// - private static readonly string startOfString = "\\G"; - private static readonly char startObject = '{'; - private static readonly char endObject = '}'; - private static readonly char startArray = '['; - private static readonly char endArray = ']'; - private static readonly char valueSeparator = ','; - private static readonly char nameSeparator = ':'; - private static readonly char[] falseValue = "false".ToCharArray(); - private static readonly char[] trueValue = "true".ToCharArray(); - private static readonly char[] nullValue = "null".ToCharArray(); - private static readonly Regex numberValue = new Regex(startOfString + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); - private static readonly Regex stringValue = new Regex(startOfString + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", RegexOptions.Multiline); - - private static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); - - private class JsonStringParser + static readonly string startOfString = "\\G"; + + static readonly char startObject = '{'; + + static readonly char endObject = '}'; + + static readonly char startArray = '['; + + static readonly char endArray = ']'; + + static readonly char valueSeparator = ','; + + static readonly char nameSeparator = ':'; + + static readonly char[] falseValue = "false".ToCharArray(); + + static readonly char[] trueValue = "true".ToCharArray(); + + static readonly char[] nullValue = "null".ToCharArray(); + + static readonly Regex numberValue = new Regex(startOfString + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); + + static readonly Regex stringValue = new Regex(startOfString + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", RegexOptions.Multiline); + + static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); + + class JsonStringParser { - public string Input { get; private set; } + public string Input { get; set; } - public char[] InputAsArray { get; private set; } - public int CurrentIndex { get; private set; } + public char[] InputAsArray { get; set; } + public int CurrentIndex { get; set; } public void Skip(int skip) => CurrentIndex += skip; @@ -78,7 +93,7 @@ internal bool ParseObject(out object output) /// /// Parses JSON member syntax (e.g. '"keyname" : null') /// - private bool ParseMember(out object output) + bool ParseMember(out object output) { output = null; if (!ParseString(out object key)) @@ -118,7 +133,7 @@ internal bool ParseArray(out object output) /// Parses a value (i.e. the right-hand side of an object member assignment or /// an element in an array) /// - private bool ParseValue(out object output) + bool ParseValue(out object output) { if (Accept(falseValue)) { @@ -144,7 +159,7 @@ private bool ParseValue(out object output) /// /// Parses a JSON string (e.g. '"foo\u1234bar\n"') /// - private bool ParseString(out object output) + bool ParseString(out object output) { output = null; if (!Accept(stringValue, out Match m)) @@ -199,7 +214,7 @@ private bool ParseString(out object output) /// Parses a number. Returns a long if the number is an integer or has an exponent, /// otherwise returns a double. /// - private bool ParseNumber(out object output) + bool ParseNumber(out object output) { output = null; if (!Accept(numberValue, out Match m)) @@ -232,7 +247,7 @@ private bool ParseNumber(out object output) /// /// Matches the string to a regex, consuming part of the string and returning the match. /// - private bool Accept(Regex matcher, out Match match) + bool Accept(Regex matcher, out Match match) { match = matcher.Match(Input, CurrentIndex); if (match.Success) @@ -243,7 +258,7 @@ private bool Accept(Regex matcher, out Match match) /// /// Find the first occurrences of a character, consuming part of the string. /// - private bool Accept(char condition) + bool Accept(char condition) { int step = 0; int strLen = InputAsArray.Length; @@ -286,7 +301,7 @@ private bool Accept(char condition) /// /// Find the first occurrences of a string, consuming part of the string. /// - private bool Accept(char[] condition) + bool Accept(char[] condition) { int step = 0; int strLen = InputAsArray.Length; diff --git a/Parse/Infrastructure/Utilities/WebUtilities.cs b/Parse/Infrastructure/Utilities/WebUtilities.cs new file mode 100644 index 00000000..e90f8e77 --- /dev/null +++ b/Parse/Infrastructure/Utilities/WebUtilities.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Parse.Infrastructure.Utilities +{ + internal static class WebUtilities + { + internal static string BuildQueryString(IDictionary parameters) => String.Join("&", (from pair in parameters let valueString = pair.Value as string select $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(String.IsNullOrEmpty(valueString) ? JsonUtilities.Encode(pair.Value) : valueString)}").ToArray()); + + internal static IDictionary DecodeQueryString(string queryString) + { + Dictionary query = new Dictionary { }; + + foreach (string pair in queryString.Split('&')) + { + string[] parts = pair.Split(new char[] { '=' }, 2); + query[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null; + } + + return query; + } + } +} diff --git a/Parse/Platform/Authentication/FacebookAuthenticationProvider.cs b/Parse/Platform/Authentication/FacebookAuthenticationProvider.cs new file mode 100644 index 00000000..43b52e85 --- /dev/null +++ b/Parse/Platform/Authentication/FacebookAuthenticationProvider.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Authentication; +using Parse.Infrastructure; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Authentication +{ + /// + /// Provides an authenticator service for the Parse SDK which uses Facebook to authorize linked Parse user access. + /// + public class FacebookAuthenticationProvider : IParseAuthenticationProvider, IServiceHubMutator + { + static Uri TokenExtensionRoute { get; } = new Uri("https://graph.facebook.com/oauth/access_token", UriKind.Absolute); + + static Uri ProfileRoute { get; } = new Uri("https://graph.facebook.com/me", UriKind.Absolute); + + TaskCompletionSource> SubmissionTaskSource { get; set; } + + CancellationToken SubmissionCancellationToken { get; set; } + + /// + /// Instantiates a . + /// + public FacebookAuthenticationProvider() + { + } + + internal Uri AuthenticationRoute { get; set; } = new Uri("https://www.facebook.com/dialog/oauth", UriKind.Absolute); + + internal Uri SuccessRoute { get; set; } = new Uri("https://www.facebook.com/connect/login_success.html", UriKind.Absolute); + + /// + /// Permissions the should ask for from Facebook, in terms of access to or mutation of user data. + /// + public IEnumerable Permissions { get; set; } + + /// + /// The identifier to make API calls to Facebook with. This is in the Facebook Developer Dashboard as some variant of "Application ID" or "API Key". + /// + public string Caller { get; set; } + + /// + /// The token for access to an individual Facebook user account. + /// + public string AccessToken { get; set; } + + /// + /// An event for when a Facebook-authentication-related web resource is loaded. + /// + public event Action Load; + + /// + /// Parses a uri, looking for a base uri that represents facebook login completion, and then + /// converting the query string into a dictionary of key-value pairs. (e.g. access_token) + /// + bool CheckIsOAuthCallback(Uri uri, out IDictionary result) + { + if (!uri.AbsoluteUri.StartsWith(SuccessRoute.AbsoluteUri) || uri.Fragment == null) + { + result = default; + return default; + } + + string fragmentOrQuery = uri.Fragment is null or { Length: 0 } ? uri.Query : uri.Fragment; + result = WebUtilities.DecodeQueryString(fragmentOrQuery.Substring(1)); + + return true; + } + + public IDictionary GetAuthenticationData(string facebookId, string accessToken, DateTime expiration) => new Dictionary + { + ["id"] = facebookId, + ["access_token"] = accessToken, + ["expiration_date"] = expiration.ToString(ParseClient.DateFormatStrings[0]) + }; + + public bool ExtractUser(IServiceHub serviceHub, Uri oAuthCallback) + { + if (CheckIsOAuthCallback(oAuthCallback, out IDictionary result)) + { + void GetUser() + { + try + { + if (result.ContainsKey("error")) + { + SubmissionTaskSource.TrySetException(new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, $"{result["error_description"]}: {result["error"]}")); + return; + } + + serviceHub.WebClient.ExecuteAsync(new WebRequest { Resource = $"{ProfileRoute}?{WebUtilities.BuildQueryString(new Dictionary { ["access_token"] = result["access_token"], ["fields"] = "id" })}", Method = "GET" }, default, default, CancellationToken.None).OnSuccess(task => SubmissionTaskSource.TrySetResult(GetAuthenticationData(JsonUtilities.DeserializeJsonText(task.Result.Item2)["id"] as string, result["access_token"], DateTime.Now + TimeSpan.FromSeconds(Int32.Parse(result["expires_in"]))))).ContinueWith(task => + { + if (task.IsFaulted) + { + SubmissionTaskSource.TrySetException(task.Exception); + } + }); + } + catch (Exception e) + { + SubmissionTaskSource.TrySetException(e); + } + } + + GetUser(); + return true; + } + + return default; + } + + /// + public Task> AuthenticateAsync(CancellationToken cancellationToken) + { + if (Caller is null) + { + throw new InvalidOperationException("You must initialize FacebookUtilities or provide a FacebookAuthenticationMutator instance to the ParseClient customization constructor before attempting a Facebook login."); + } + + if (SubmissionTaskSource != null) + { + SubmissionTaskSource.TrySetCanceled(); + } + + TaskCompletionSource> tcs = new TaskCompletionSource> { }; + + SubmissionCancellationToken = cancellationToken; + SubmissionTaskSource = tcs; + + cancellationToken.Register(() => tcs.TrySetCanceled()); + + Action navigateHandler = Load; + + if (navigateHandler != null) + { + Dictionary parameters = new Dictionary() + { + ["redirect_uri"] = SuccessRoute.AbsoluteUri, + ["response_type"] = "token", + ["display"] = "popup", + ["client_id"] = Caller + }; + + if (Permissions != null) + { + parameters["scope"] = String.Join(",", Permissions.ToArray()); + } + + navigateHandler(new Uri(AuthenticationRoute, $"?{WebUtilities.BuildQueryString(parameters)}")); + } + return tcs.Task; + } + + /// + public void Deauthenticate() => AccessToken = default; + + /// + public bool RestoreAuthentication(IDictionary authData) + { + if (authData == null) + { + Deauthenticate(); + } + else + { + AccessToken = authData["access_token"] as string; + } + + return true; + } + + /// + public string Name => "facebook"; + + void IServiceHubMutator.Mutate(ref IMutableServiceHub mutableHub, in IServiceHub consumableHub, Stack futureMutators) => mutableHub.InitializeFacebookAuthentication(authenticator: this); + + bool IServiceHubMutator.Valid => Caller is { Length: > 0 }; + } +} diff --git a/Parse/Platform/Configuration/ParseConfiguration.cs b/Parse/Platform/Configuration/ParseConfiguration.cs index 90b43a1f..c048e29c 100644 --- a/Parse/Platform/Configuration/ParseConfiguration.cs +++ b/Parse/Platform/Configuration/ParseConfiguration.cs @@ -70,7 +70,7 @@ public bool TryGetValue(string key, out T result) /// The value for the key. virtual public object this[string key] => Properties[key]; - IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary + IDictionary IJsonConvertible.ConvertToJson() => new Dictionary { ["params"] = NoObjectsEncoder.Instance.Encode(Properties, Services) }; diff --git a/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs b/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs index 8870c1b1..98b6d209 100644 --- a/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs +++ b/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs @@ -18,7 +18,7 @@ internal class ParseCurrentConfigurationController : IParseCurrentConfigurationC ParseConfiguration CurrentConfiguration { get; set; } - ICacheController StorageController { get; } + ICacheController CacheController { get; } IParseDataDecoder Decoder { get; } @@ -27,27 +27,27 @@ internal class ParseCurrentConfigurationController : IParseCurrentConfigurationC /// public ParseCurrentConfigurationController(ICacheController storageController, IParseDataDecoder decoder) { - StorageController = storageController; + CacheController = storageController; Decoder = decoder; TaskQueue = new TaskQueue { }; } - public Task GetCurrentConfigAsync(IServiceHub serviceHub) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration is { } ? Task.FromResult(CurrentConfiguration) : StorageController.LoadAsync().OnSuccess(task => + public Task GetCurrentConfigAsync(IServiceHub serviceHub) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration is { } ? Task.FromResult(CurrentConfiguration) : CacheController.LoadAsync().OnSuccess(task => { task.Result.TryGetValue(CurrentConfigurationKey, out object data); - return CurrentConfiguration = data is string { } configuration ? Decoder.BuildConfiguration(ParseClient.DeserializeJsonString(configuration), serviceHub) : new ParseConfiguration(serviceHub); + return CurrentConfiguration = data is string { } configuration ? Decoder.BuildConfiguration(JsonUtilities.DeserializeJsonText(configuration), serviceHub) : new ParseConfiguration(serviceHub); })), CancellationToken.None).Unwrap(); public Task SetCurrentConfigAsync(ParseConfiguration target) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { CurrentConfiguration = target; - return StorageController.LoadAsync().OnSuccess(task => task.Result.AddAsync(CurrentConfigurationKey, ParseClient.SerializeJsonString(((IJsonConvertible) target).ConvertToJSON()))); + return CacheController.LoadAsync().OnSuccess(task => task.Result.AddAsync(CurrentConfigurationKey, JsonUtilities.SerializeToJsonText(((IJsonConvertible) target).ConvertToJson()))); }).Unwrap().Unwrap(), CancellationToken.None); public Task ClearCurrentConfigAsync() => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { CurrentConfiguration = null; - return StorageController.LoadAsync().OnSuccess(task => task.Result.RemoveAsync(CurrentConfigurationKey)); + return CacheController.LoadAsync().OnSuccess(task => task.Result.RemoveAsync(CurrentConfigurationKey)); }).Unwrap().Unwrap(), CancellationToken.None); public Task ClearCurrentConfigInMemoryAsync() => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration = null), CancellationToken.None); diff --git a/Parse/Platform/Files/ParseFile.cs b/Parse/Platform/Files/ParseFile.cs index d7871ccf..166dd24d 100644 --- a/Parse/Platform/Files/ParseFile.cs +++ b/Parse/Platform/Files/ParseFile.cs @@ -138,7 +138,7 @@ public ParseFile(string name, Stream data, string mimeType = null) #endregion - IDictionary IJsonConvertible.ConvertToJSON() + IDictionary IJsonConvertible.ConvertToJson() { if (IsDirty) { diff --git a/Parse/Platform/Location/ParseGeoPoint.cs b/Parse/Platform/Location/ParseGeoPoint.cs index da1b6190..998a9e14 100644 --- a/Parse/Platform/Location/ParseGeoPoint.cs +++ b/Parse/Platform/Location/ParseGeoPoint.cs @@ -88,7 +88,7 @@ public ParseGeoDistance DistanceTo(ParseGeoPoint point) return new ParseGeoDistance(2 * Math.Asin(Math.Sqrt(a))); } - IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary { + IDictionary IJsonConvertible.ConvertToJson() => new Dictionary { {"__type", "GeoPoint"}, {nameof(latitude), Latitude}, {nameof(longitude), Longitude} diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs index 31121bd6..746bc9bd 100644 --- a/Parse/Platform/ParseClient.cs +++ b/Parse/Platform/ParseClient.cs @@ -16,7 +16,7 @@ namespace Parse /// ParseClient contains static functions that handle global /// configuration for the Parse library. /// - public class ParseClient : CustomServiceHub, IServiceHubComposer + public class ParseClient : CustomServiceHub, IServiceHubBuilder { /// /// Contains, in order, the official ISO date and time format strings, and two modified versions that account for the possibility that the server-side string processing mechanism removed trailing zeroes. @@ -54,22 +54,22 @@ public class ParseClient : CustomServiceHub, IServiceHubComposer /// The server URI provided in the Parse dashboard. /// The .NET Key provided in the Parse dashboard. /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner. - /// A set of implementation instances to tweak the behaviour of the SDK. - public ParseClient(string applicationID, string serverURI, string key, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) : this(new ServerConnectionData { ApplicationID = applicationID, ServerURI = serverURI, Key = key }, serviceHub, configurators) { } + /// A set of implementation instances to tweak the behaviour of the SDK. + public ParseClient(string applicationID, string serverURI, string key, IServiceHub serviceHub = default, params IServiceHubMutator[] mutators) : this(new ServerConnectionData { ApplicationID = applicationID, ServerURI = serverURI, Key = key }, serviceHub, mutators) { } /// /// Creates a new and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call on it in your application entry point. This allows you to access . /// - /// The configuration to initialize Parse with. + /// The configuration to initialize Parse with. /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner. - /// A set of implementation instances to tweak the behaviour of the SDK. - public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) + /// A set of implementation instances to tweak the behaviour of the SDK. + public ParseClient(IServerConnectionData serverConnectionData, IServiceHub serviceHub = default, params IServiceHubMutator[] mutators) { Services = serviceHub is { } ? new OrchestrationServiceHub { Custom = serviceHub, Default = new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } } : new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } as IServiceHub; - IServerConnectionData GenerateServerConnectionData() => configuration switch + IServerConnectionData GenerateServerConnectionData() => serverConnectionData switch { - null => throw new ArgumentNullException(nameof(configuration)), + null => throw new ArgumentNullException(nameof(serverConnectionData)), ServerConnectionData { Test: true, ServerURI: { } } data => data, ServerConnectionData { Test: true } data => new ServerConnectionData { @@ -80,17 +80,17 @@ public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = Key = data.Key, ServerURI = "https://api.parse.com/1/" }, - { ServerURI: "https://api.parse.com/1/" } => throw new InvalidOperationException("Since the official parse server has shut down, you must specify a URI that points to a hosted instance."), + { ServerURI: "https://api.parse.com/1/" } => throw new InvalidOperationException("Since the official Parse server has shut down, you must specify a URI that points to a hosted instance."), { ApplicationID: { }, ServerURI: { }, Key: { } } data => data, _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.") }; - if (configurators is { Length: int length } && length > 0) + if (mutators is { Length: int length } && length > 0) { Services = serviceHub switch { - IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, configurators), - { } => BuildHub(default, Services, configurators) + IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, mutators), + { } => BuildHub(default, Services, mutators) }; } @@ -100,12 +100,11 @@ public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = /// /// Initializes a instance using the set on the 's implementation instance. /// - public ParseClient() => Services = (Instance ?? throw new InvalidOperationException("A ParseClient instance with an initializer service must first be publicized in order for the default constructor to be used.")).Services.Cloner.BuildHub(Instance.Services, this); + public ParseClient() => Services = (Instance ?? throw new InvalidOperationException("A ParseClient instance with an initializer service must first be publicized in order for the default constructor to be used.")).Services.Cloner.CloneHub(Instance.Services, this); /// /// Sets this instance as the template to create new instances from. /// - ///// Declares that the current instance should be the publicly-accesible . public void Publicize() { lock (Mutex) @@ -116,36 +115,22 @@ public void Publicize() static object Mutex { get; } = new object { }; - internal static string BuildQueryString(IDictionary parameters) => String.Join("&", (from pair in parameters let valueString = pair.Value as string select $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(String.IsNullOrEmpty(valueString) ? JsonUtilities.Encode(pair.Value) : valueString)}").ToArray()); - - internal static IDictionary DecodeQueryString(string queryString) + public IServiceHub BuildHub(IMutableServiceHub baseHub = default, IServiceHub extension = default, params IServiceHubMutator[] mutators) { - Dictionary query = new Dictionary { }; + OrchestrationServiceHub orchestrationServiceHub = new OrchestrationServiceHub { Custom = baseHub ??= new MutableServiceHub { }, Default = extension ?? new ServiceHub { } }; - foreach (string pair in queryString.Split('&')) + Stack validMutators = new Stack(mutators.Where(mutator => mutator.Valid).Reverse()); + while (validMutators.Count > 0 && GetMutator() is { } mutator) { - string[] parts = pair.Split(new char[] { '=' }, 2); - query[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null; - } - - return query; - } - - internal static IDictionary DeserializeJsonString(string jsonData) => JsonUtilities.Parse(jsonData) as IDictionary; - - internal static string SerializeJsonString(IDictionary jsonData) => JsonUtilities.Encode(jsonData); - - public IServiceHub BuildHub(IMutableServiceHub target = default, IServiceHub extension = default, params IServiceHubMutator[] configurators) - { - OrchestrationServiceHub orchestrationServiceHub = new OrchestrationServiceHub { Custom = target ??= new MutableServiceHub { }, Default = extension ?? new ServiceHub { } }; - - foreach (IServiceHubMutator mutator in configurators.Where(configurator => configurator.Valid)) - { - mutator.Mutate(ref target, orchestrationServiceHub); - orchestrationServiceHub.Custom = target; + if (mutator is { Valid: true }) + { + mutator.Mutate(ref baseHub, orchestrationServiceHub, validMutators); + orchestrationServiceHub.Custom = baseHub; + } } return orchestrationServiceHub; + IServiceHubMutator GetMutator() => validMutators.Pop(); } } } diff --git a/Parse/Platform/Queries/ParseQueryController.cs b/Parse/Platform/Queries/ParseQueryController.cs index e56c0107..c7c04141 100644 --- a/Parse/Platform/Queries/ParseQueryController.cs +++ b/Parse/Platform/Queries/ParseQueryController.cs @@ -43,6 +43,6 @@ public Task FirstAsync(ParseQuery query, ParseUser user, Can return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(task => (task.Result["results"] as IList).FirstOrDefault() as IDictionary is Dictionary item && item != null ? ParseObjectCoder.Instance.Decode(item, Decoder, user.Services) : null); } - Task> FindAsync(string className, IDictionary parameters, string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"classes/{Uri.EscapeDataString(className)}?{ParseClient.BuildQueryString(parameters)}", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(t => t.Result.Item2); + Task> FindAsync(string className, IDictionary parameters, string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"classes/{Uri.EscapeDataString(className)}?{WebUtilities.BuildQueryString(parameters)}", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(t => t.Result.Item2); } } diff --git a/Parse/Platform/Relations/ParseRelation.cs b/Parse/Platform/Relations/ParseRelation.cs index ff977ae3..d85e961c 100644 --- a/Parse/Platform/Relations/ParseRelation.cs +++ b/Parse/Platform/Relations/ParseRelation.cs @@ -64,7 +64,7 @@ internal void Remove(ParseObject entity) TargetClassName = change.TargetClassName; } - IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary + IDictionary IJsonConvertible.ConvertToJson() => new Dictionary { ["__type"] = "Relation", ["className"] = TargetClassName diff --git a/Parse/Platform/Security/ParseACL.cs b/Parse/Platform/Security/ParseACL.cs index 4258c5fc..087ac93a 100644 --- a/Parse/Platform/Security/ParseACL.cs +++ b/Parse/Platform/Security/ParseACL.cs @@ -51,7 +51,7 @@ public ParseACL(ParseUser owner) SetWriteAccess(owner, true); } - IDictionary IJsonConvertible.ConvertToJSON() + IDictionary IJsonConvertible.ConvertToJson() { Dictionary result = new Dictionary(); foreach (string user in readers.Union(writers)) diff --git a/Parse/Platform/Users/ParseUser.cs b/Parse/Platform/Users/ParseUser.cs index 74f7c914..32b29fe1 100644 --- a/Parse/Platform/Users/ParseUser.cs +++ b/Parse/Platform/Users/ParseUser.cs @@ -177,16 +177,20 @@ internal Task LogOutAsync(Task toAwait, CancellationToken cancellationToken) return Task.FromResult(0); } - // Cleanup in-memory session. + // TODO: Consider use of toAwait to make sure all tasks are finished before the revokeSession task is executed. + // Cleans the stored session. MutateState(mutableClone => mutableClone.ServerData.Remove("sessionToken")); Task revokeSessionTask = Services.RevokeSessionAsync(oldSessionToken, cancellationToken); return Task.WhenAll(revokeSessionTask, Services.CurrentUserController.LogOutAsync(Services, cancellationToken)); } - internal Task UpgradeToRevocableSessionAsync() => UpgradeToRevocableSessionAsync(CancellationToken.None); - - internal Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), cancellationToken); + /// + /// Uses a revocable session token for this instance. + /// + /// The cancellation token to use to halt this task if needed before it has finished. + /// A task which will be finished once the move to a revocable session token has occurred. + public Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), cancellationToken); internal Task UpgradeToRevocableSessionAsync(Task toAwait, CancellationToken cancellationToken) { @@ -257,12 +261,12 @@ internal void SynchronizeAllAuthData() foreach (KeyValuePair> pair in authData) { - SynchronizeAuthData(GetProvider(pair.Key)); + SynchronizeAuthenticationData(GetProvider(pair.Key)); } } } - internal void SynchronizeAuthData(IParseAuthenticationProvider provider) + internal void SynchronizeAuthenticationData(IParseAuthenticationProvider authenticator) { bool restorationSuccess = false; @@ -270,57 +274,72 @@ internal void SynchronizeAuthData(IParseAuthenticationProvider provider) { IDictionary> authData = AuthData; - if (authData == null || provider == null) + if (authData == null || authenticator == null) { return; } - if (authData.TryGetValue(provider.AuthType, out IDictionary data)) + if (authData.TryGetValue(authenticator.Name, out IDictionary data)) { - restorationSuccess = provider.RestoreAuthentication(data); + restorationSuccess = authenticator.RestoreAuthentication(data); } } if (!restorationSuccess) { - UnlinkFromAsync(provider.AuthType, CancellationToken.None); + // TODO: Check if lack of await here causes issues. + + UnlinkFromServiceAsync(authenticator.Name, CancellationToken.None); } } - internal Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => + /// + /// Links a user to a service. + /// + /// The name of the service to link the user to. + /// The authentication data for the service. + /// The cancellation token which should be used if the link task needs to be halted. + /// + public Task LinkToServiceAsync(string name, IDictionary data, CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => { IDictionary> authData = AuthData; if (authData == null) { - authData = AuthData = new Dictionary>(); + authData = AuthData = new Dictionary> { }; } - authData[authType] = data; + authData[name] = data; AuthData = authData; return SaveAsync(cancellationToken); }, cancellationToken); - internal Task LinkWithAsync(string authType, CancellationToken cancellationToken) + /// + /// Links a user to a service if the SDK was initialized with an with the data needed to authenticate this user. + /// + /// The name of the service to link the user to. + /// The cancellation token which should be used if the link task needs to be halted. + /// A task which completes once the user has been linked to the service + public Task LinkToServiceWithInitializedAuthenticationProviderAsync(string name, CancellationToken cancellationToken = default) { - IParseAuthenticationProvider provider = GetProvider(authType); - return provider.AuthenticateAsync(cancellationToken).OnSuccess(t => LinkWithAsync(authType, t.Result, cancellationToken)).Unwrap(); + IParseAuthenticationProvider authenticator = GetProvider(name); + return authenticator.AuthenticateAsync(cancellationToken).OnSuccess(task => LinkToServiceAsync(name, task.Result, cancellationToken)).Unwrap(); } /// /// Unlinks a user from a service. /// - internal Task UnlinkFromAsync(string authType, CancellationToken cancellationToken) => LinkWithAsync(authType, null, cancellationToken); + public Task UnlinkFromServiceAsync(string name, CancellationToken cancellationToken = default) => LinkToServiceAsync(name, default, cancellationToken); /// /// Checks whether a user is linked to a service. /// - internal bool IsLinked(string authType) + public bool CheckLinkedToService(string name) { lock (Mutex) { - return AuthData != null && AuthData.ContainsKey(authType) && AuthData[authType] != null; + return AuthData != null && AuthData.ContainsKey(name) && AuthData[name] != null; } } } diff --git a/Parse/Platform/Users/ParseUserController.cs b/Parse/Platform/Users/ParseUserController.cs index 0d2097e0..9ad1a6af 100644 --- a/Parse/Platform/Users/ParseUserController.cs +++ b/Parse/Platform/Users/ParseUserController.cs @@ -27,7 +27,7 @@ public class ParseUserController : IParseUserController public Task SignUpAsync(IObjectState state, IDictionary operations, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("classes/_User", method: "POST", data: serviceHub.GenerateJSONObjectForSaving(operations)), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = true)); - public Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"login?{ParseClient.BuildQueryString(new Dictionary { [nameof(username)] = username, [nameof(password)] = password })}", method: "GET", data: null), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created)); + public Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"login?{WebUtilities.BuildQueryString(new Dictionary { [nameof(username)] = username, [nameof(password)] = password })}", method: "GET", data: null), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created)); public Task LogInAsync(string authType, IDictionary data, IServiceHub serviceHub, CancellationToken cancellationToken = default) { diff --git a/Parse/Utilities/PushServiceExtensions.cs b/Parse/Utilities/PushServiceExtensions.cs index caa32d53..e538a203 100644 --- a/Parse/Utilities/PushServiceExtensions.cs +++ b/Parse/Utilities/PushServiceExtensions.cs @@ -8,6 +8,9 @@ namespace Parse { + /// + /// Utilities to control and interact with the Parse Push service via this SDK. + /// public static class PushServiceExtensions { /// diff --git a/Parse/Utilities/UserServiceExtensions.cs b/Parse/Utilities/UserServiceExtensions.cs index 01eef386..c07fb624 100644 --- a/Parse/Utilities/UserServiceExtensions.cs +++ b/Parse/Utilities/UserServiceExtensions.cs @@ -175,26 +175,19 @@ internal static bool GetIsRevocableSessionEnabled(this IServiceHub serviceHub) #endregion - /// - /// Requests a password reset email to be sent to the specified email address associated with the - /// user account. This email allows the user to securely reset their password on the Parse site. - /// - /// The email address associated with the user that forgot their password. - public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email) => RequestPasswordResetAsync(serviceHub, email, CancellationToken.None); - /// /// Requests a password reset email to be sent to the specified email address associated with the /// user account. This email allows the user to securely reset their password on the Parse site. /// /// The email address associated with the user that forgot their password. /// The cancellation token. - public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email, CancellationToken cancellationToken) => serviceHub.UserController.RequestPasswordResetAsync(email, cancellationToken); + public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email, CancellationToken cancellationToken = default) => serviceHub.UserController.RequestPasswordResetAsync(email, cancellationToken); - public static Task LogInWithAsync(this IServiceHub serviceHub, string authType, IDictionary data, CancellationToken cancellationToken) + public static Task AuthenticateWithServiceAsync(this IServiceHub serviceHub, string authenticationServiceName, IDictionary data, CancellationToken cancellationToken) { ParseUser user = null; - return serviceHub.UserController.LogInAsync(authType, data, serviceHub, cancellationToken).OnSuccess(task => + return serviceHub.UserController.LogInAsync(authenticationServiceName, data, serviceHub, cancellationToken).OnSuccess(task => { user = serviceHub.GenerateObjectFromState(task.Result, "_User"); @@ -205,7 +198,7 @@ public static Task LogInWithAsync(this IServiceHub serviceHub, string user.AuthData = new Dictionary>(); } - user.AuthData[authType] = data; + user.AuthData[authenticationServiceName] = data; #warning Check if SynchronizeAllAuthData should accept an IServiceHub for consistency on which actions take place on which IServiceHub implementation instance. @@ -216,22 +209,22 @@ public static Task LogInWithAsync(this IServiceHub serviceHub, string }).Unwrap().OnSuccess(t => user); } - public static Task LogInWithAsync(this IServiceHub serviceHub, string authType, CancellationToken cancellationToken) + public static Task AuthenticateWithServiceAsync(this IServiceHub serviceHub, string authenticationServiceName, CancellationToken cancellationToken) { - IParseAuthenticationProvider provider = ParseUser.GetProvider(authType); - return provider.AuthenticateAsync(cancellationToken).OnSuccess(authData => LogInWithAsync(serviceHub, authType, authData.Result, cancellationToken)).Unwrap(); + IParseAuthenticationProvider provider = ParseUser.GetProvider(authenticationServiceName); + return provider.AuthenticateAsync(cancellationToken).OnSuccess(authData => AuthenticateWithServiceAsync(serviceHub, authenticationServiceName, authData.Result, cancellationToken)).Unwrap(); } - internal static void RegisterProvider(this IServiceHub serviceHub, IParseAuthenticationProvider provider) + internal static void SetAuthenticationProvider(this IServiceHub serviceHub, IParseAuthenticationProvider provider) { - ParseUser.Authenticators[provider.AuthType] = provider; - ParseUser curUser = GetCurrentUser(serviceHub); + ParseUser.Authenticators[provider.Name] = provider; + ParseUser currentUser = GetCurrentUser(serviceHub); - if (curUser != null) + if (currentUser != null) { #warning Check if SynchronizeAllAuthData should accept an IServiceHub for consistency on which actions take place on which IServiceHub implementation instance. - curUser.SynchronizeAuthData(provider); + currentUser.SynchronizeAuthenticationData(provider); } } }