Bug 1859954 - Use XP_DARWIN rather than XP_MACOS in PHC r=glandium
[gecko.git] / services / fxaccounts / FxAccountsProfileClient.sys.mjs
blob7ae1bd95db1390bbedc36791c41f8444008a7df1
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * A client to fetch profile information for a Firefox Account.
7  */
8 "use strict;";
10 import {
11   ERRNO_NETWORK,
12   ERRNO_PARSE,
13   ERRNO_UNKNOWN_ERROR,
14   ERROR_CODE_METHOD_NOT_ALLOWED,
15   ERROR_MSG_METHOD_NOT_ALLOWED,
16   ERROR_NETWORK,
17   ERROR_PARSE,
18   ERROR_UNKNOWN,
19   log,
20   SCOPE_PROFILE,
21   SCOPE_PROFILE_WRITE,
22 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
24 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
26 const fxAccounts = getFxAccountsSingleton();
27 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
29 /**
30  * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information.
31  *
32  * @param {Object} options Options
33  *   @param {String} options.serverURL
34  *   The URL of the profile server to query.
35  *   Example: https://profile.accounts.firefox.com/v1
36  *   @param {String} options.token
37  *   The bearer token to access the profile server
38  * @constructor
39  */
40 export var FxAccountsProfileClient = function (options) {
41   if (!options || !options.serverURL) {
42     throw new Error("Missing 'serverURL' configuration option");
43   }
45   this.fxai = options.fxai || fxAccounts._internal;
47   try {
48     this.serverURL = new URL(options.serverURL);
49   } catch (e) {
50     throw new Error("Invalid 'serverURL'");
51   }
52   log.debug("FxAccountsProfileClient: Initialized");
55 FxAccountsProfileClient.prototype = {
56   /**
57    * {nsIURI}
58    * The server to fetch profile information from.
59    */
60   serverURL: null,
62   /**
63    * Interface for making remote requests.
64    */
65   _Request: RESTRequest,
67   /**
68    * Remote request helper which abstracts authentication away.
69    *
70    * @param {String} path
71    *        Profile server path, i.e "/profile".
72    * @param {String} [method]
73    *        Type of request, e.g. "GET".
74    * @param {String} [etag]
75    *        Optional ETag used for caching purposes.
76    * @param {Object} [body]
77    *        Optional request body, to be sent as application/json.
78    * @return Promise
79    *         Resolves: {body: Object, etag: Object} Successful response from the Profile server.
80    *         Rejects: {FxAccountsProfileClientError} Profile client error.
81    * @private
82    */
83   async _createRequest(path, method = "GET", etag = null, body = null) {
84     method = method.toUpperCase();
85     let token = await this._getTokenForRequest(method);
86     try {
87       return await this._rawRequest(path, method, token, etag, body);
88     } catch (ex) {
89       if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
90         throw ex;
91       }
92       // it's an auth error - assume our token expired and retry.
93       log.info(
94         "Fetching the profile returned a 401 - revoking our token and retrying"
95       );
96       await this.fxai.removeCachedOAuthToken({ token });
97       token = await this._getTokenForRequest(method);
98       // and try with the new token - if that also fails then we fail after
99       // revoking the token.
100       try {
101         return await this._rawRequest(path, method, token, etag, body);
102       } catch (ex) {
103         if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
104           throw ex;
105         }
106         log.info(
107           "Retry fetching the profile still returned a 401 - revoking our token and failing"
108         );
109         await this.fxai.removeCachedOAuthToken({ token });
110         throw ex;
111       }
112     }
113   },
115   /**
116    * Helper to get an OAuth token for a request.
117    *
118    * OAuth tokens are cached, so it's fine to call this for each request.
119    *
120    * @param {String} [method]
121    *        Type of request, i.e "GET".
122    * @return Promise
123    *         Resolves: Object containing "scope", "token" and "key" properties
124    *         Rejects: {FxAccountsProfileClientError} Profile client error.
125    * @private
126    */
127   async _getTokenForRequest(method) {
128     let scope = SCOPE_PROFILE;
129     if (method === "POST") {
130       scope = SCOPE_PROFILE_WRITE;
131     }
132     return this.fxai.getOAuthToken({ scope });
133   },
135   /**
136    * Remote "raw" request helper - doesn't handle auth errors and tokens.
137    *
138    * @param {String} path
139    *        Profile server path, i.e "/profile".
140    * @param {String} method
141    *        Type of request, i.e "GET".
142    * @param {String} token
143    * @param {String} etag
144    * @param {Object} payload
145    *        The payload of the request, if any.
146    * @return Promise
147    *         Resolves: {body: Object, etag: Object} Successful response from the Profile server
148                         or null if 304 is hit (same ETag).
149    *         Rejects: {FxAccountsProfileClientError} Profile client error.
150    * @private
151    */
152   async _rawRequest(path, method, token, etag = null, payload = null) {
153     let profileDataUrl = this.serverURL + path;
154     let request = new this._Request(profileDataUrl);
156     request.setHeader("Authorization", "Bearer " + token);
157     request.setHeader("Accept", "application/json");
158     if (etag) {
159       request.setHeader("If-None-Match", etag);
160     }
162     if (method != "GET" && method != "POST") {
163       // method not supported
164       throw new FxAccountsProfileClientError({
165         error: ERROR_NETWORK,
166         errno: ERRNO_NETWORK,
167         code: ERROR_CODE_METHOD_NOT_ALLOWED,
168         message: ERROR_MSG_METHOD_NOT_ALLOWED,
169       });
170     }
171     try {
172       await request.dispatch(method, payload);
173     } catch (error) {
174       throw new FxAccountsProfileClientError({
175         error: ERROR_NETWORK,
176         errno: ERRNO_NETWORK,
177         message: error.toString(),
178       });
179     }
181     let body = null;
182     try {
183       if (request.response.status == 304) {
184         return null;
185       }
186       body = JSON.parse(request.response.body);
187     } catch (e) {
188       throw new FxAccountsProfileClientError({
189         error: ERROR_PARSE,
190         errno: ERRNO_PARSE,
191         code: request.response.status,
192         message: request.response.body,
193       });
194     }
196     // "response.success" means status code is 200
197     if (!request.response.success) {
198       throw new FxAccountsProfileClientError({
199         error: body.error || ERROR_UNKNOWN,
200         errno: body.errno || ERRNO_UNKNOWN_ERROR,
201         code: request.response.status,
202         message: body.message || body,
203       });
204     }
205     return {
206       body,
207       etag: request.response.headers.etag,
208     };
209   },
211   /**
212    * Retrieve user's profile from the server
213    *
214    * @param {String} [etag]
215    *        Optional ETag used for caching purposes. (may generate a 304 exception)
216    * @return Promise
217    *         Resolves: {body: Object, etag: Object} Successful response from the '/profile' endpoint.
218    *         Rejects: {FxAccountsProfileClientError} profile client error.
219    */
220   fetchProfile(etag) {
221     log.debug("FxAccountsProfileClient: Requested profile");
222     return this._createRequest("/profile", "GET", etag);
223   },
227  * Normalized profile client errors
228  * @param {Object} [details]
229  *        Error details object
230  *   @param {number} [details.code]
231  *          Error code
232  *   @param {number} [details.errno]
233  *          Error number
234  *   @param {String} [details.error]
235  *          Error description
236  *   @param {String|null} [details.message]
237  *          Error message
238  * @constructor
239  */
240 export var FxAccountsProfileClientError = function (details) {
241   details = details || {};
243   this.name = "FxAccountsProfileClientError";
244   this.code = details.code || null;
245   this.errno = details.errno || ERRNO_UNKNOWN_ERROR;
246   this.error = details.error || ERROR_UNKNOWN;
247   this.message = details.message || null;
251  * Returns error object properties
253  * @returns {{name: *, code: *, errno: *, error: *, message: *}}
254  * @private
255  */
256 FxAccountsProfileClientError.prototype._toStringFields = function () {
257   return {
258     name: this.name,
259     code: this.code,
260     errno: this.errno,
261     error: this.error,
262     message: this.message,
263   };
267  * String representation of a profile client error
269  * @returns {String}
270  */
271 FxAccountsProfileClientError.prototype.toString = function () {
272   return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";