Co-authored-by: Patrick Ngo <pango@microsoft.com>
This commit is contained in:
pmngo 2024-03-05 10:47:45 -05:00 коммит произвёл GitHub
Родитель 7100b5928e
Коммит 87fca6b3d6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
15 изменённых файлов: 653 добавлений и 0 удалений

Просмотреть файл

@ -4,6 +4,7 @@
// license information.
//------------------------------------------------------------
using System.Text.Json;
using Xunit;
namespace Microsoft.Azure.NotificationHubs.Tests
@ -151,6 +152,34 @@ namespace Microsoft.Azure.NotificationHubs.Tests
Assert.Equal(ChannelUri, installation.PushChannel);
Assert.Equal(NotificationPlatform.Mpns, installation.Platform);
}
[Fact]
public void CanCreateBrowserInstallation()
{
var installation = new BrowserInstallation();
Assert.Equal(NotificationPlatform.Browser, installation.Platform);
}
[Fact]
public void CanCreateBrowserInstallationWithBrowserPushSubscription()
{
var browserPushSubscription = new BrowserPushSubscription
{
Endpoint = "foo",
P256DH = "bar",
Auth = "baz",
};
var installation = new BrowserInstallation(InstallationId, browserPushSubscription);
var pushSubscription = JsonSerializer.Deserialize<BrowserPushSubscription>(installation.PushChannel);
Assert.NotNull(pushSubscription);
Assert.Equal(browserPushSubscription.Endpoint, pushSubscription.Endpoint);
Assert.Equal(browserPushSubscription.P256DH, pushSubscription.P256DH);
Assert.Equal(browserPushSubscription.Auth, pushSubscription.Auth);
Assert.Equal(NotificationPlatform.Browser, installation.Platform);
}
}
}

Просмотреть файл

@ -18,6 +18,7 @@ namespace Microsoft.Azure.NotificationHubs.DotNetCore.Tests
Assert.Equal($"application/json;charset={Encoding.UTF8.WebName}", new TemplateNotification(new Dictionary<string, string>()).ContentType);
Assert.Equal("application/json", new AdmNotification("{\"data\":{\"key1\":\"value1\"}}").ContentType);
Assert.Equal("application/x-www-form-urlencoded", new BaiduNotification("{\"title\":\"Title\",\"description\":\"Description\"}").ContentType);
Assert.Equal($"application/json;charset={Encoding.UTF8.WebName}", new BrowserNotification("{\"title\": \"Title\", \"message\": \"Hello World!\"}").ContentType);
}
[Theory]

Просмотреть файл

@ -0,0 +1,100 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for
// license information.
//------------------------------------------------------------
using System.Runtime.Serialization;
using Microsoft.Azure.NotificationHubs.Messaging;
namespace Microsoft.Azure.NotificationHubs
{
[DataContract(Name = ManagementStrings.BrowserCredential, Namespace = ManagementStrings.Namespace)]
public class BrowserCredential : PnsCredential
{
internal const string AppPlatformName = "browser";
/// <summary>
/// Gets or sets web push subject
/// </summary>
public string Subject
{
get { return base[nameof(Subject)]; }
set { base[nameof(Subject)] = value; }
}
/// <summary>
/// Gets or sets VAPID public key.
/// </summary>
public string VapidPublicKey
{
get { return base[nameof(VapidPublicKey)]; }
set { base[nameof(VapidPublicKey)] = value; }
}
/// <summary>
/// Gets or sets VAPID private key.
/// </summary>
public string VapidPrivateKey
{
get { return base[nameof(VapidPrivateKey)]; }
set { base[nameof(VapidPrivateKey)] = value; }
}
internal override string AppPlatform => AppPlatformName;
/// <summary>
/// Specifies whether the credential is equal with the specific object.
/// </summary>
/// <returns>
/// true if the credential is equal with the specific object; otherwise, false.
/// </returns>
/// <param name="other">The other object to compare.</param>
public override bool Equals(object other)
{
var otherCredential = other as BrowserCredential;
if (otherCredential == null)
{
return false;
}
return (otherCredential.Subject == Subject && otherCredential.VapidPublicKey == VapidPublicKey && otherCredential.VapidPrivateKey == VapidPrivateKey);
}
/// <summary>
/// Retrieves the hash code for the credentials.
/// </summary>
/// <returns>
/// The hash code for the credentials.
/// </returns>
public override int GetHashCode()
{
return unchecked(Subject.GetHashCode() ^ VapidPublicKey.GetHashCode() ^ VapidPrivateKey.GetHashCode());
}
/// <summary>Validates the browser credential.</summary>
/// <param name="allowLocalMockPns">true to allow local mock PNS; otherwise, false.</param>
protected override void OnValidate(bool allowLocalMockPns)
{
if (Properties == null || Properties.Count > 3)
{
throw new InvalidDataContractException(SRClient.BrowserRequiredProperties);
}
if (string.IsNullOrWhiteSpace(Subject))
{
throw new InvalidDataContractException(SRClient.BrowserSubjectNotSpecified);
}
if (string.IsNullOrWhiteSpace(VapidPublicKey))
{
throw new InvalidDataContractException(SRClient.BrowserVapidPublicKeyNotSpecified);
}
if (string.IsNullOrWhiteSpace(VapidPrivateKey))
{
throw new InvalidDataContractException(SRClient.BrowserVapidPrivateKeyNotSpecified);
}
}
}
}

Просмотреть файл

@ -0,0 +1,52 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for
// license information.
//------------------------------------------------------------
using System;
using System.Text.Json;
namespace Microsoft.Azure.NotificationHubs
{
/// <summary>
/// This class represents a browser installation.
/// </summary>
public class BrowserInstallation : Installation
{
/// <summary>
/// Creates a new instance of the <see cref="T:Microsoft.Azure.NotificationHubs.BrowserInstallation"/> class.
/// </summary>
public BrowserInstallation()
{
Platform = NotificationPlatform.Browser;
}
/// <summary>
/// Creates a new instance of the <see cref="T:Microsoft.Azure.NotificationHubs.BrowserInstallation"/> class.
/// </summary>
/// <param name="installationId">The unique identifier for the installation.</param>
/// <param name="browserPushSubscription">The browser push subscription.</param>
public BrowserInstallation(string installationId, BrowserPushSubscription browserPushSubscription) : this()
{
InstallationId = installationId ?? throw new ArgumentNullException(nameof(installationId));
if (string.IsNullOrWhiteSpace(browserPushSubscription.Endpoint))
{
throw new ArgumentNullException(nameof(browserPushSubscription.Endpoint));
}
if (string.IsNullOrWhiteSpace(browserPushSubscription.P256DH))
{
throw new ArgumentNullException(nameof(browserPushSubscription.P256DH));
}
if (string.IsNullOrWhiteSpace(browserPushSubscription.Auth))
{
throw new ArgumentNullException(nameof(browserPushSubscription.Auth));
}
PushChannel = JsonSerializer.Serialize(browserPushSubscription);
}
}
}

Просмотреть файл

@ -0,0 +1,39 @@
//----------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for
// license information.
//----------------------------------------------------------------
using System.Text;
namespace Microsoft.Azure.NotificationHubs
{
/// <summary>
/// Represents a browser notification.
/// </summary>
public class BrowserNotification : Notification, INativeNotification
{
private static string contentType = $"application/json;charset={Encoding.UTF8.WebName}";
/// <summary>
/// Initializes a new instance of the <see cref="T:Microsoft.Azure.NotificationHubs.BrowserNotification"/> class.
/// </summary>
/// <param name="payload">The notification payload.</param>
public BrowserNotification(string payload) : base(null, null, contentType)
{
Body = payload;
}
/// <summary>
/// The platform type.
/// </summary>
protected override string PlatformType => BrowserCredential.AppPlatformName;
/// <summary>
/// Validate and populates the headers.
/// </summary>
protected override void OnValidateAndPopulateHeaders()
{
}
}
}

Просмотреть файл

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace Microsoft.Azure.NotificationHubs
{
public class BrowserPushSubscription
{
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; }
[JsonPropertyName("p256dh")]
public string P256DH { get; set; }
[JsonPropertyName("auth")]
public string Auth { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,181 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for
// license information.
//------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.Json;
using Microsoft.Azure.NotificationHubs.Messaging;
namespace Microsoft.Azure.NotificationHubs
{
[DataContract(Name = ManagementStrings.BrowserRegistrationDescription, Namespace = ManagementStrings.Namespace)]
public class BrowserRegistrationDescription : RegistrationDescription
{
private const string PlatformName = "browser";
private BrowserPushSubscription _browserPushSubscription;
/// <summary>
/// Browser push endpoint from PNS.
/// </summary>
[DataMember(Name = ManagementStrings.BrowserEndpoint, Order = 2001, IsRequired = true)]
public string Endpoint
{
get
{
return _browserPushSubscription?.Endpoint;
}
set
{
if (_browserPushSubscription != null)
{
_browserPushSubscription.Endpoint = value;
}
}
}
/// <summary>
/// Browser push P256DH key from PNS.
/// </summary>
[DataMember(Name = ManagementStrings.BrowserP256DH, Order = 2002, IsRequired = true)]
public string P256DH
{
get
{
return _browserPushSubscription?.P256DH;
}
set
{
if (_browserPushSubscription != null)
{
_browserPushSubscription.P256DH = value;
}
}
}
/// <summary>
/// Browser push authentication secret from PNS.
/// </summary>
[DataMember(Name = ManagementStrings.BrowserAuth, Order = 2003, IsRequired = true)]
public string Auth
{
get
{
return _browserPushSubscription?.Auth;
}
set
{
if (_browserPushSubscription != null)
{
_browserPushSubscription.Auth = value;
}
}
}
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.BrowserRegistrationDescription"/> class copying fields from another instance.
/// </summary>
/// <param name="sourceRegistration">Another <see cref="T:Microsoft.Azure.NotificationHubs.BrowserRegistrationDescription"/> instance whose fields values will be copied.</param>
public BrowserRegistrationDescription(BrowserRegistrationDescription sourceRegistration) : base(sourceRegistration)
{
var browserPushSubscription = new BrowserPushSubscription
{
Endpoint = sourceRegistration.Endpoint,
P256DH = sourceRegistration.P256DH,
Auth = sourceRegistration.Auth,
};
ValidateBrowserPushSubscription(browserPushSubscription);
_browserPushSubscription = browserPushSubscription;
}
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.BrowserRegistrationDescription"/> class.
/// </summary>
/// <param name="browserPushSubscription">The browser push subscription.</param>
public BrowserRegistrationDescription(BrowserPushSubscription browserPushSubscription) : this(browserPushSubscription, null)
{
}
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.BrowserRegistrationDescription"/> class.
/// </summary>
/// <param name="browserPushSubscription">The browser push subscription.</param>
/// <param name="tags">Collection of tags. Tags can be used for audience targeting purposes.</param>
public BrowserRegistrationDescription(BrowserPushSubscription browserPushSubscription, IEnumerable<string> tags)
: this(string.Empty, browserPushSubscription, tags)
{
}
internal BrowserRegistrationDescription(string notificationHubPath, BrowserPushSubscription browserPushSubscription, IEnumerable<string> tags) : base(notificationHubPath)
{
ValidateBrowserPushSubscription(browserPushSubscription);
_browserPushSubscription = new BrowserPushSubscription
{
Endpoint = browserPushSubscription.Endpoint,
P256DH = browserPushSubscription.P256DH,
Auth = browserPushSubscription.Auth,
};
if (tags != null)
{
Tags = new HashSet<string>(tags);
}
}
internal override string AppPlatForm
{
get { return BrowserCredential.AppPlatformName; }
}
internal override string RegistrationType
{
get { return BrowserCredential.AppPlatformName; }
}
internal override string PlatformType
{
get { return BrowserCredential.AppPlatformName; }
}
internal override string GetPnsHandle() => JsonSerializer.Serialize(_browserPushSubscription);
internal override void SetPnsHandle(string pnsHandle)
{
var browserPushSubscription = JsonSerializer.Deserialize<BrowserPushSubscription>(pnsHandle);
Endpoint = browserPushSubscription.Endpoint;
P256DH = browserPushSubscription.P256DH;
Auth = browserPushSubscription.Auth;
_browserPushSubscription = browserPushSubscription;
}
internal override RegistrationDescription Clone()
{
return new BrowserRegistrationDescription(this);
}
private void ValidateBrowserPushSubscription(BrowserPushSubscription browserPushSubscription)
{
if (string.IsNullOrWhiteSpace(browserPushSubscription.Endpoint))
{
throw new ArgumentNullException(nameof(browserPushSubscription.Endpoint));
}
if (string.IsNullOrWhiteSpace(browserPushSubscription.P256DH))
{
throw new ArgumentNullException(nameof(browserPushSubscription.P256DH));
}
if (string.IsNullOrWhiteSpace(browserPushSubscription.Auth))
{
throw new ArgumentNullException(nameof(browserPushSubscription.Auth));
}
}
}
}

Просмотреть файл

@ -0,0 +1,158 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for
// license information.
//------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Azure.NotificationHubs.Messaging;
namespace Microsoft.Azure.NotificationHubs
{
public class BrowserTemplateRegistrationDescription : BrowserRegistrationDescription
{
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.BrowserTemplateRegistrationDescription"/> class by copying fields from the given instance
/// </summary>
/// <param name="sourceRegistration">Another <see cref="T:Microsoft.Azure.NotificationHubs.BrowserTemplateRegistrationDescription"/> instance whose fields values are copied from</param>
public BrowserTemplateRegistrationDescription(BrowserTemplateRegistrationDescription sourceRegistration)
: base(sourceRegistration)
{
BodyTemplate = sourceRegistration.BodyTemplate;
TemplateName = sourceRegistration.TemplateName;
}
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.BrowserTemplateRegistrationDescription"/> class
/// </summary>
/// <param name="browserPushSubscription">The browser push subscription.</param>
public BrowserTemplateRegistrationDescription(BrowserPushSubscription browserPushSubscription)
: base(browserPushSubscription)
{
}
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.BrowserTemplateRegistrationDescription"/> class
/// </summary>
/// <param name="browserPushSubscription">The browser push subscription.</param>
/// <param name="jsonPayload">Payload template.</param>
public BrowserTemplateRegistrationDescription(BrowserPushSubscription browserPushSubscription, string jsonPayload)
: this(string.Empty, browserPushSubscription, jsonPayload, null)
{
}
/// <summary>
/// Creates instance of <see cref="T:Microsoft.Azure.NotificationHubs.FcmV1TemplateRegistrationDescription"/> class
/// </summary>
/// <param name="browserPushSubscription">The browser push subscription.</param>
/// <param name="jsonPayload">Payload template.</param>
/// <param name="tags">Collection of tags. Tags can be used for audience targeting purposes.</param>
public BrowserTemplateRegistrationDescription(BrowserPushSubscription browserPushSubscription, string jsonPayload, IEnumerable<string> tags)
: this(string.Empty, browserPushSubscription, jsonPayload, tags)
{
}
internal BrowserTemplateRegistrationDescription(string notificationHubPath, BrowserPushSubscription browserPushSubscription, string jsonPayload, IEnumerable<string> tags)
: base(notificationHubPath, browserPushSubscription, tags)
{
if (string.IsNullOrWhiteSpace(jsonPayload))
{
throw new ArgumentNullException(nameof(jsonPayload));
}
BodyTemplate = new CDataMember(jsonPayload);
}
/// <summary>
/// Gets or sets a template body for notification payload which may contain placeholders to be filled in with actual data during the send operation
/// </summary>
[DataMember(Name = ManagementStrings.BodyTemplate, IsRequired = true, Order = 3001)]
public CDataMember BodyTemplate { get; set; }
/// <summary>
/// Gets or sets a name of the template
/// </summary>
[DataMember(Name = ManagementStrings.TemplateName, IsRequired = false, Order = 3002)]
public string TemplateName { get; set; }
internal override string AppPlatForm
{
get
{
return BrowserCredential.AppPlatformName;
}
}
internal override string RegistrationType
{
get
{
return RegistrationDescription.TemplateRegistrationType;
}
}
internal override string PlatformType
{
get
{
return BrowserCredential.AppPlatformName + RegistrationDescription.TemplateRegistrationType;
}
}
internal override void OnValidate()
{
base.OnValidate();
try
{
using (XmlReader reader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(BodyTemplate), new XmlDictionaryReaderQuotas()))
{
XDocument payloadDocument = XDocument.Load(reader);
foreach (XElement element in payloadDocument.Root.DescendantsAndSelf())
{
foreach (XAttribute attribute in element.Attributes())
{
ExpressionEvaluator.Validate(attribute.Value);
}
if (!element.HasElements && !string.IsNullOrEmpty(element.Value))
{
ExpressionEvaluator.Validate(element.Value);
}
}
}
}
catch (InvalidOperationException)
{
// We get an ugly and misleading error message when this exception happens -> The XmlReader state should be Interactive.
// Hence we are using a more friendlier error message
throw new XmlException(SRClient.FailedToDeserializeBodyTemplate);
}
ValidateTemplateName();
}
private void ValidateTemplateName()
{
if (TemplateName != null)
{
if (TemplateName.Length > RegistrationSDKHelper.TemplateMaxLength)
{
throw new InvalidDataContractException(string.Format(SRClient.TemplateNameLengthExceedsLimit, RegistrationSDKHelper.TemplateMaxLength));
}
}
}
internal override RegistrationDescription Clone()
{
return new BrowserTemplateRegistrationDescription(this);
}
}
}

Просмотреть файл

@ -41,6 +41,8 @@ namespace Microsoft.Azure.NotificationHubs.Messaging
public const string MpnsTemplateRegistrationDescription = "MpnsTemplateRegistrationDescription";
public const string AdmRegistrationDescription = "AdmRegistrationDescription";
public const string AdmTemplateRegistrationDescription = "AdmTemplateRegistrationDescription";
public const string BrowserRegistrationDescription = "BrowserRegistrationDescription";
public const string BrowserTemplateRegistrationDescription = "BrowserTemplateRegistrationDescription";
public const string PnsCredential = "PnsCredential";
public const string PnsCredentials = "PnsCredentials";
public const string GcmCredential = "GcmCredential";
@ -50,12 +52,16 @@ namespace Microsoft.Azure.NotificationHubs.Messaging
public const string WnsCredential = "WnsCredential";
public const string ApnsCredential = "ApnsCredential";
public const string AdmCredential = "AdmCredential";
public const string BrowserCredential = "BrowserCredential";
public const string GcmRegistrationId = "GcmRegistrationId";
public const string FcmRegistrationId = "FcmRegistrationId";
public const string FcmV1RegistrationId = "FcmV1RegistrationId";
public const string BaiduUserId = "BaiduUserId";
public const string BaiduChannelId = "BaiduChannelId";
public const string MpnsCredential = "MpnsCredential";
public const string BrowserEndpoint = "Endpoint";
public const string BrowserP256DH = "P256DH";
public const string BrowserAuth = "Auth";
public const string MpnsHeaders = "MpnsHeaders";
public const string ApnsHeaders = "ApnsHeaders";
public const string MpnsHeader = "MpnsHeader";
@ -105,6 +111,7 @@ namespace Microsoft.Azure.NotificationHubs.Messaging
public const string FcmV1OutcomeCounts = "FcmV1OutcomeCounts";
public const string AdmOutcomeCounts = "AdmOutcomeCounts";
public const string BaiduOutcomeCounts = "BaiduOutcomeCounts";
public const string BrowserOutcomeCounts = "BrowserOutcomeCounts";
public const string NotificationBody = "NotificationBody";
public const string NotificationOutcomeCollection = "NotificationOutcomeCollection";

Просмотреть файл

@ -42,6 +42,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.2" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -413,6 +413,20 @@ namespace Microsoft.Azure.NotificationHubs
set;
}
/// <summary>
/// Gets or sets the browser credential.
/// </summary>
///
/// <returns>
/// The browser credential.
/// </returns>
[DataMember(Name = ManagementStrings.BrowserCredential, IsRequired = false, EmitDefaultValue = false, Order = 1018)]
public BrowserCredential BrowserCredential
{
get;
set;
}
/// <summary>
/// Gets/Sets any User Metadata associated with the NotificationHub.
/// </summary>

Просмотреть файл

@ -57,6 +57,12 @@ namespace Microsoft.Azure.NotificationHubs
[EnumMember(Value = "baidu")]
Baidu=6,
/// <summary>
/// Browser Installation Platform
/// </summary>
[EnumMember(Value = "browser")]
Browser=8,
/// <summary>
/// FCM V1 Installation Platform
/// </summary>

Просмотреть файл

@ -22,6 +22,7 @@ namespace Microsoft.Azure.NotificationHubs
[KnownType(typeof(WnsCredential))]
[KnownType(typeof(AdmCredential))]
[KnownType(typeof(FcmV1Credential))]
[KnownType(typeof(BrowserCredential))]
public abstract class PnsCredential : EntityDescription
{
internal PnsCredential()

Просмотреть файл

@ -456,6 +456,42 @@ namespace Microsoft.Azure.NotificationHubs {
}
}
/// <summary>
/// Looks up a localized string similar to Subject, VapidPublicKey, and VapidPrivateKey are required..
/// </summary>
internal static string BrowserRequiredProperties {
get {
return ResourceManager.GetString("BrowserRequiredProperties", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Subject is either not specified or invalid..
/// </summary>
internal static string BrowserSubjectNotSpecified {
get {
return ResourceManager.GetString("BrowserSubjectNotSpecified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to VAPID private key is either not specified or invalid..
/// </summary>
internal static string BrowserVapidPrivateKeyNotSpecified {
get {
return ResourceManager.GetString("BrowserVapidPrivateKeyNotSpecified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to VAPID public key is either not specified or invalid..
/// </summary>
internal static string BrowserVapidPublicKeyNotSpecified {
get {
return ResourceManager.GetString("BrowserVapidPublicKeyNotSpecified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The buffer has already been reclaimed..
/// </summary>

Просмотреть файл

@ -1309,4 +1309,16 @@
<data name="FcmV1RequiredProperties" xml:space="preserve">
<value>PrivateKey, ProjectId, and ClientEmail are required.</value>
</data>
<data name="BrowserRequiredProperties" xml:space="preserve">
<value>Subject, VapidPublicKey, and VapidPrivateKey are required.</value>
</data>
<data name="BrowserSubjectNotSpecified" xml:space="preserve">
<value>Subject is either not specified or invalid.</value>
</data>
<data name="BrowserVapidPrivateKeyNotSpecified" xml:space="preserve">
<value>VAPID private key is either not specified or invalid.</value>
</data>
<data name="BrowserVapidPublicKeyNotSpecified" xml:space="preserve">
<value>VAPID public key is either not specified or invalid.</value>
</data>
</root>