From 799a9e810d1b7001ab0020933cf2d25c8ee3467d Mon Sep 17 00:00:00 2001 From: jonath92 <49979415+jonath92@users.noreply.github.com> Date: Sat, 8 Aug 2020 01:35:30 +0200 Subject: [PATCH] All: Resolves #1266: Add support for OneDrive for Business (#3433) --- ReactNativeClient/lib/SyncTargetOneDrive.js | 20 +++++++++++-- .../lib/file-api-driver-onedrive.js | 20 +++++++------ ReactNativeClient/lib/onedrive-api.js | 29 +++++++++++++++++-- ReactNativeClient/lib/registry.js | 15 ---------- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/ReactNativeClient/lib/SyncTargetOneDrive.js b/ReactNativeClient/lib/SyncTargetOneDrive.js index 57a197239f..ce6c6a2eb1 100644 --- a/ReactNativeClient/lib/SyncTargetOneDrive.js +++ b/ReactNativeClient/lib/SyncTargetOneDrive.js @@ -82,6 +82,15 @@ class SyncTargetOneDrive extends BaseSyncTarget { } async initFileApi() { + let context = Setting.value(`sync.${this.syncTargetId()}.context`); + context = context === '' ? null : JSON.parse(context); + let accountProperties = context ? context.accountProperties : null; + if (!accountProperties) { + accountProperties = await this.api_.execAccountPropertiesRequest(); + context ? context.accountProperties = accountProperties : context = { accountProperties: accountProperties }; + Setting.setValue(`sync.${this.syncTargetId()}.context`, JSON.stringify(context)); + } + this.api_.setAccountProperties(accountProperties); const appDir = await this.api().appDirectory(); const fileApi = new FileApi(appDir, new FileApiDriverOneDrive(this.api())); fileApi.setSyncTargetId(this.syncTargetId()); @@ -90,8 +99,15 @@ class SyncTargetOneDrive extends BaseSyncTarget { } async initSynchronizer() { - if (!(await this.isAuthenticated())) throw new Error('User is not authentified'); - return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType')); + try { + if (!(await this.isAuthenticated())) throw new Error('User is not authentified'); + return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType')); + } catch (error) { + BaseSyncTarget.dispatch({ type: 'SYNC_REPORT_UPDATE', report: { errors: [error] } }); + throw error; + } + + } } diff --git a/ReactNativeClient/lib/file-api-driver-onedrive.js b/ReactNativeClient/lib/file-api-driver-onedrive.js index bf5ecf94c3..a7d939f1a4 100644 --- a/ReactNativeClient/lib/file-api-driver-onedrive.js +++ b/ReactNativeClient/lib/file-api-driver-onedrive.js @@ -217,7 +217,9 @@ class FileApiDriverOneDrive { }; const freshStartDelta = () => { - const url = `${this.makePath_(path)}:/delta`; + // Business Accounts are only allowed to make delta requests to the root. For some reason /delta gives an error for personal accounts and :/delta an error for business accounts + const accountProperties = this.api_.accountProperties_; + const url = (accountProperties.accountType === 'business') ? `/drives/${accountProperties.driveId}/root/delta` : `${this.makePath_(path)}:/delta`; const query = this.itemFilter_(); query.select += ',deleted'; return { url: url, query: query }; @@ -265,14 +267,14 @@ class FileApiDriverOneDrive { const items = []; - // The delta API might return things that happen in subdirectories of the root and we don't want to - // deal with these since all the files we're interested in are at the root (The .resource dir - // is special since it's managed directly by the clients and resources never change - only the - // associated .md file at the root is synced). So in the loop below we check that the parent is - // indeed the root, otherwise the item is skipped. - // (Not sure but it's possible the delta API also returns events for files that are copied outside - // of the app directory and later deleted or modified. We also don't want to deal with - // these files during sync). + // The delta API might return things that happens in subdirectories and outside of the joplin directory. + // We don't want to deal with these since all the files we're interested in are at the root of the joplin directory + // (The .resource dir is special since it's managed directly by the clients and resources never change - only the + // associated .md file at the root is synced). So in the loop below we check that the parent is indeed the joplin + // directory, otherwise the item is skipped. + // At OneDrive for Business delta requests can only make at the root of OneDrive. Not sure but it's possible that + // the delta API also returns events for files that are copied outside of the app directory and later deleted or + // modified when using OneDrive Personal). for (let i = 0; i < response.value.length; i++) { const v = response.value[i]; diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index e67adcf460..33b41863c4 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -13,6 +13,7 @@ class OneDriveApi { this.clientId_ = clientId; this.clientSecret_ = clientSecret; this.auth_ = null; + this.accountProperties_ = null; this.isPublic_ = isPublic; this.listeners_ = { authRefreshed: [], @@ -73,14 +74,15 @@ class OneDriveApi { } async appDirectory() { - const r = await this.execJson('GET', '/drive/special/approot'); + const driveId = this.accountProperties_.driveId; + const r = await this.execJson('GET', `/me/drives/${driveId}/special/approot`); return `${r.parentReference.path}/${r.name}`; } authCodeUrl(redirectUri) { const query = { client_id: this.clientId_, - scope: 'files.readwrite offline_access', + scope: 'files.readwrite offline_access sites.readwrite.all', response_type: 'code', redirect_uri: redirectUri, }; @@ -320,6 +322,29 @@ class OneDriveApi { throw new Error(`Could not execute request after multiple attempts: ${method} ${url}`); } + setAccountProperties(accountProperties) { + this.accountProperties_ = accountProperties; + } + + async execAccountPropertiesRequest() { + const response = await shim.fetch('https://graph.microsoft.com/v1.0/me/drive', { + method: 'GET', + headers: { + 'Authorization': this.token(), + }, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Could not retrieve account details (drive ID, Account type): ${response.status}: ${response.statusText}: ${text}`); + } else { + const data = await response.json(); + const accountProperties = { accountType: data.driveType, driveId: data.id }; + return accountProperties; + } + + } + async execJson(method, path, query, data) { const response = await this.exec(method, path, query, data); const errorResponseText = await response.text(); diff --git a/ReactNativeClient/lib/registry.js b/ReactNativeClient/lib/registry.js index 28d5bded56..eb0d93c2b6 100644 --- a/ReactNativeClient/lib/registry.js +++ b/ReactNativeClient/lib/registry.js @@ -2,7 +2,6 @@ const { Logger } = require('lib/logger.js'); const Setting = require('lib/models/Setting.js'); const { shim } = require('lib/shim.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); -const { _ } = require('lib/locale.js'); const reg = {}; @@ -141,20 +140,6 @@ reg.scheduleSync = async (delay = null, syncOptions = null) => { } catch (error) { reg.logger().info('Could not run background sync:'); reg.logger().info(error); - - // Special case to display OneDrive Business error. This is the full error that's received when trying to use a OneDrive Business account: - // - // {"error":"invalid_client","error_description":"AADSTS50011: The reply address 'http://localhost:1917' does not match the reply addresses configured for - // the application: 'cbabb902-d276-4ea4-aa88-062a5889d6dc'. More details: not specified\r\nTrace ID: 6e63dac6-8b37-47e2-bd1b-4768f8713400\r\nCorrelation - // ID: acfd6503-8d97-4349-ae2e-e7a19dd7b6bc\r\nTimestamp: 2017-12-01 13:35:55Z","error_codes":[50011],"timestamp":"2017-12-01 13:35:55Z","trace_id": - // "6e63dac6-8b37-47e2-bd1b-4768f8713400","correlation_id":"acfd6503-8d97-4349-ae2e-e7a19dd7b6bc"}: TOKEN: null Error: {"error":"invalid_client", - // "error_description":"AADSTS50011: The reply address 'http://localhost:1917' does not match the reply addresses configured for the application: - // 'cbabb902-d276-4ea4-aa88-062a5889d6dc'. More details: not specified\r\nTrace ID: 6e63dac6-8b37-47e2-bd1b-4768f8713400\r\nCorrelation ID - // acfd6503-8d97-4349-ae2e-e7a19dd7b6bc\r\nTimestamp: 2017-12-01 13:35:55Z","error_codes":[50011],"timestamp":"2017-12-01 13:35:55Z","trace_id": - // "6e63dac6-8b37-47e2-bd1b-4768f8713400","correlation_id":"acfd6503-8d97-4349-ae2e-e7a19dd7b6bc"} - if (error && error.message && error.message.indexOf('"invalid_client"') >= 0) { - reg.showErrorMessageBox(_('Could not synchronise with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.')); - } } reg.setupRecurrentSync(); promiseResolve();