2019-12-13 01:16:34 +00:00
const { Logger } = require ( 'lib/logger.js' );
const { shim } = require ( 'lib/shim.js' );
const JoplinError = require ( 'lib/JoplinError' );
const { rtrimSlashes } = require ( 'lib/path-utils.js' );
const base64 = require ( 'base-64' );
2020-02-04 22:09:34 +00:00
const { _ } = require ( 'lib/locale' );
2019-12-13 01:16:34 +00:00
interface JoplinServerApiOptions {
username : Function ,
password : Function ,
baseUrl : Function ,
}
export default class JoplinServerApi {
logger_ :any ;
options_ :JoplinServerApiOptions ;
kvStore_ :any ;
constructor ( options :JoplinServerApiOptions ) {
this . logger_ = new Logger ();
this . options_ = options ;
this . kvStore_ = null ;
}
setLogger ( l :any ) {
this . logger_ = l ;
}
logger () : any {
return this . logger_ ;
}
setKvStore ( v :any ) {
this . kvStore_ = v ;
}
kvStore() {
if ( ! this . kvStore_ ) throw new Error ( 'JoplinServerApi.kvStore_ is not set!!' );
return this . kvStore_ ;
}
authToken () : string {
if ( ! this . options_ . username () || ! this . options_ . password ()) return null ;
try {
// Note: Non-ASCII passwords will throw an error about Latin1 characters - https://github.com/laurent22/joplin/issues/246
// Tried various things like the below, but it didn't work on React Native:
// return base64.encode(utf8.encode(this.options_.username() + ':' + this.options_.password()));
return base64 . encode ( ` ${ this . options_ . username () } : ${ this . options_ . password () } ` );
} catch ( error ) {
error . message = `Cannot encode username/password: ${ error . message } ` ;
throw error ;
}
}
baseUrl () : string {
return rtrimSlashes ( this . options_ . baseUrl ());
}
static baseUrlFromNextcloudWebDavUrl ( webDavUrl :string ) {
// http://nextcloud.local/remote.php/webdav/Joplin
// http://nextcloud.local/index.php/apps/joplin/api
const splitted = webDavUrl . split ( '/remote.php/webdav' );
if ( splitted . length !== 2 ) throw new Error ( `Unsupported WebDAV URL format: ${ webDavUrl } ` );
return ` ${ splitted [ 0 ] } /index.php/apps/joplin/api` ;
}
syncTargetId ( settings :any ) {
const s = settings [ 'sync.5.syncTargets' ][ settings [ 'sync.5.path' ]];
if ( ! s ) throw new Error ( `Joplin Nextcloud app not configured for URL: ${ this . baseUrl () } ` );
return s . uuid ;
}
static connectionErrorMessage ( error :any ) {
const msg = error && error . message ? error . message : 'Unknown error' ;
return _ ( 'Could not connect to the Joplin Nextcloud app. Please check the configuration in the Synchronisation config screen. Full error was:\n\n%s' , msg );
}
async setupSyncTarget ( webDavUrl :string ) {
return this . exec ( 'POST' , 'sync_targets' , {
webDavUrl : webDavUrl ,
});
}
requestToCurl_ ( url :string , options :any ) {
let output = [];
output . push ( 'curl' );
output . push ( '-v' );
if ( options . method ) output . push ( `-X ${ options . method } ` );
if ( options . headers ) {
for ( let n in options . headers ) {
if ( ! options . headers . hasOwnProperty ( n )) continue ;
output . push ( ` ${ '-H ' + '"' }${ n } : ${ options . headers [ n ] } "` );
}
}
if ( options . body ) output . push ( ` ${ '--data ' + '\'' }${ options . body } '` );
output . push ( url );
return output . join ( ' ' );
}
async exec ( method :string , path :string = '' , body :any = null , headers :any = null , options :any = null ) : Promise < any > {
if ( headers === null ) headers = {};
if ( options === null ) options = {};
const authToken = this . authToken ();
if ( authToken ) headers [ 'Authorization' ] = `Basic ${ authToken } ` ;
headers [ 'Content-Type' ] = 'application/json' ;
if ( typeof body === 'object' && body !== null ) body = JSON . stringify ( body );
const fetchOptions :any = {};
fetchOptions . headers = headers ;
fetchOptions . method = method ;
if ( options . path ) fetchOptions . path = options . path ;
if ( body ) fetchOptions . body = body ;
const url = ` ${ this . baseUrl () } / ${ path } ` ;
let response = null ;
// console.info('WebDAV Call', method + ' ' + url, headers, options);
console . info ( this . requestToCurl_ ( url , fetchOptions ));
if ( typeof body === 'string' ) fetchOptions . headers [ 'Content-Length' ] = ` ${ shim . stringByteLength ( body ) } ` ;
response = await shim . fetch ( url , fetchOptions );
const responseText = await response . text ();
let responseJson_ :any = null ;
const loadResponseJson = async () => {
if ( ! responseText ) return null ;
if ( responseJson_ ) return responseJson_ ;
2019-12-18 15:32:19 +00:00
try {
return JSON . parse ( responseText );
} catch ( error ) {
throw new Error ( `Cannot parse JSON: ${ responseText . substr ( 0 , 8192 ) } ` );
}
2019-12-13 01:16:34 +00:00
};
const newError = ( message :string , code :number = 0 ) => {
return new JoplinError ( ` ${ method } ${ path } : ${ message } ( ${ code } )` , code );
};
if ( ! response . ok ) {
let json = null ;
try {
json = await loadResponseJson ();
} catch ( error ) {
2019-12-18 15:32:19 +00:00
throw newError ( `Unknown error: ${ responseText . substr ( 0 , 8192 ) } ` , response . status );
2019-12-13 01:16:34 +00:00
}
const trace = json . stacktrace ? ` \ n ${ json . stacktrace } ` : '' ;
2019-12-18 15:32:19 +00:00
let message = json . error ;
if ( ! message ) message = responseText . substr ( 0 , 8192 );
throw newError ( message + trace , response . status );
2019-12-13 01:16:34 +00:00
}
const output = await loadResponseJson ();
return output ;
}
}