import { shim } from 'lib/shim.js'; import { stringify } from 'query-string'; class OneDriveApi { constructor(clientId, clientSecret) { this.clientId_ = clientId; this.clientSecret_ = clientSecret; this.auth_ = null; this.listeners_ = { 'authRefreshed': [], }; } dispatch(eventName, param) { let ls = this.listeners_[eventName]; for (let i = 0; i < ls.length; i++) { ls[i](param); } } on(eventName, callback) { this.listeners_[eventName].push(callback); } tokenBaseUrl() { return 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; } auth() { return this.auth_; } setAuth(auth) { this.auth_ = auth; } token() { return this.auth_ ? this.auth_.access_token : null; } clientId() { return this.clientId_; } clientSecret() { return this.clientSecret_; } // possibleOAuthDancePorts() { // return [1917, 9917, 8917]; // } async appDirectory() { let r = await this.execJson('GET', '/drive/special/approot'); return r.parentReference.path + '/' + r.name; } authCodeUrl(redirectUri) { let query = { client_id: this.clientId_, scope: 'files.readwrite offline_access', response_type: 'code', redirect_uri: redirectUri, }; return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + stringify(query); } oneDriveErrorResponseToError(errorResponse) { if (!errorResponse) return new Error('Undefined error'); if (errorResponse.error) { let e = errorResponse.error; let output = new Error(e.message); if (e.code) output.code = e.code; if (e.innerError) output.innerError = e.innerError; return output; } else { return new Error(JSON.stringify(errorResponse)); } } async exec(method, path, query = null, data = null, options = null) { method = method.toUpperCase(); if (!options) options = {}; if (!options.headers) options.headers = {}; if (method != 'GET') { options.method = method; } if (method == 'PATCH' || method == 'POST') { options.headers['Content-Type'] = 'application/json'; if (data) data = JSON.stringify(data); } let url = path; // In general, `path` contains a path relative to the base URL, but in some // cases the full URL is provided (for example, when it's a URL that was // retrieved from the API). if (url.indexOf('https://') !== 0) url = 'https://graph.microsoft.com/v1.0' + path; if (query) { url += url.indexOf('?') < 0 ? '?' : '&'; url += stringify(query); } if (data) options.body = data; // Rare error (one Google hit) - maybe repeat the request when it happens? // { error: // { code: 'generalException', // message: 'An error occurred in the data store.', // innerError: // { 'request-id': 'b4310552-c18a-45b1-bde1-68e2c2345eef', // date: '2017-06-29T00:15:50' } } } for (let i = 0; i < 5; i++) { options.headers['Authorization'] = 'bearer ' + this.token(); let response = await shim.fetch(url, options); if (!response.ok) { let errorResponse = await response.json(); let error = this.oneDriveErrorResponseToError(errorResponse); if (error.code == 'InvalidAuthenticationToken') { await this.refreshAccessToken(); continue; } else { error.request = method + ' ' + url + ' ' + JSON.stringify(query) + ' ' + JSON.stringify(data) + ' ' + JSON.stringify(options); throw error; } } return response; } throw new Error('Could not execute request after multiple attempts: ' + method + ' ' + url); } async execJson(method, path, query, data) { let response = await this.exec(method, path, query, data); let output = await response.json(); return output; } async execText(method, path, query, data) { let response = await this.exec(method, path, query, data); let output = await response.text(); return output; } async refreshAccessToken() { if (!this.auth_) throw new Error('Cannot refresh token: authentication data is missing'); let body = new shim.FormData(); body.append('client_id', this.clientId()); body.append('client_secret', this.clientSecret()); body.append('refresh_token', this.auth_.refresh_token); body.append('redirect_uri', 'http://localhost:1917'); body.append('grant_type', 'refresh_token'); let options = { method: 'POST', body: body, }; this.auth_ = null; let response = await shim.fetch(this.tokenBaseUrl(), options); if (!response.ok) { let msg = await response.text(); throw new Error(msg); } this.auth_ = await response.json(); this.dispatch('authRefreshed', this.auth_); } // async oauthDance(targetConsole = null) { // if (targetConsole === null) targetConsole = console; // this.auth_ = null; // let ports = this.possibleOAuthDancePorts(); // let port = null; // for (let i = 0; i < ports.length; i++) { // let inUse = await tcpPortUsed.check(ports[i]); // if (!inUse) { // port = ports[i]; // break; // } // } // if (!port) throw new Error('All potential ports are in use - please report the issue at https://github.com/laurent22/joplin'); // let authCodeUrl = this.authCodeUrl('http://localhost:' + port); // return new Promise((resolve, reject) => { // let server = http.createServer(); // let errorMessage = null; // server.on('request', (request, response) => { // const query = urlParser.parse(request.url, true).query; // function writeResponse(code, message) { // response.writeHead(code, {"Content-Type": "text/html"}); // response.write(message); // response.end(); // } // if (!query.code) return writeResponse(400, '"code" query parameter is missing'); // let body = new shim.FormData(); // body.append('client_id', this.clientId()); // body.append('client_secret', this.clientSecret()); // body.append('code', query.code ? query.code : ''); // body.append('redirect_uri', 'http://localhost:' + port.toString()); // body.append('grant_type', 'authorization_code'); // let options = { // method: 'POST', // body: body, // }; // fetch(this.tokenBaseUrl(), options).then((r) => { // if (!r.ok) { // errorMessage = 'Could not retrieve auth code: ' + r.status + ': ' + r.statusText; // writeResponse(400, errorMessage); // targetConsole.log(''); // targetConsole.log(errorMessage); // server.destroy(); // return; // } // return r.json().then((json) => { // this.auth_ = json; // writeResponse(200, 'The application has been authorised - you may now close this browser tab.'); // targetConsole.log(''); // targetConsole.log('The application has been successfully authorised.'); // server.destroy(); // }); // }); // }); // server.on('close', () => { // if (errorMessage) { // reject(new Error(errorMessage)); // } else { // resolve(this.auth_); // } // }); // server.listen(port); // enableServerDestroy(server); // targetConsole.log('Please open this URL in your browser to authentify the application:'); // targetConsole.log(''); // targetConsole.log(authCodeUrl); // }); // } } export { OneDriveApi };