You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Server: Add support for sharing notes via a link
This commit is contained in:
@@ -1,163 +1,190 @@
|
||||
import shim from './shim';
|
||||
import { _ } from './locale';
|
||||
import Logger from './Logger';
|
||||
const { rtrimSlashes } = require('./path-utils.js');
|
||||
const JoplinError = require('./JoplinError');
|
||||
const { rtrimSlashes } = require('./path-utils');
|
||||
const base64 = require('base-64');
|
||||
const { stringify } = require('query-string');
|
||||
|
||||
interface JoplinServerApiOptions {
|
||||
username: Function;
|
||||
password: Function;
|
||||
baseUrl: Function;
|
||||
interface Options {
|
||||
baseUrl(): string;
|
||||
username(): string;
|
||||
password(): string;
|
||||
}
|
||||
|
||||
enum ExecOptionsResponseFormat {
|
||||
Json = 'json',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
enum ExecOptionsTarget {
|
||||
String = 'string',
|
||||
File = 'file',
|
||||
}
|
||||
|
||||
interface ExecOptions {
|
||||
responseFormat?: ExecOptionsResponseFormat;
|
||||
target?: ExecOptionsTarget;
|
||||
path?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export default class JoplinServerApi {
|
||||
|
||||
logger_: any;
|
||||
options_: JoplinServerApiOptions;
|
||||
kvStore_: any;
|
||||
private options_: Options;
|
||||
private session_: any;
|
||||
|
||||
constructor(options: JoplinServerApiOptions) {
|
||||
this.logger_ = new Logger();
|
||||
public constructor(options: Options) {
|
||||
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 {
|
||||
private baseUrl() {
|
||||
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`;
|
||||
private async session() {
|
||||
// TODO: handle invalid session
|
||||
if (this.session_) return this.session_;
|
||||
|
||||
this.session_ = await this.exec('POST', 'api/sessions', null, {
|
||||
email: this.options_.username(),
|
||||
password: this.options_.password(),
|
||||
});
|
||||
|
||||
return this.session_;
|
||||
}
|
||||
|
||||
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;
|
||||
private async sessionId() {
|
||||
const session = await this.session();
|
||||
return session ? session.id : '';
|
||||
}
|
||||
|
||||
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,
|
||||
public async shareFile(pathOrId: string) {
|
||||
return this.exec('POST', 'api/shares', null, {
|
||||
file_id: pathOrId,
|
||||
type: 1, // ShareType.Link
|
||||
});
|
||||
}
|
||||
|
||||
requestToCurl_(url: string, options: any) {
|
||||
const output = [];
|
||||
output.push('curl');
|
||||
output.push('-v');
|
||||
if (options.method) output.push(`-X ${options.method}`);
|
||||
if (options.headers) {
|
||||
for (const 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(' ');
|
||||
public static connectionErrorMessage(error: any) {
|
||||
const msg = error && error.message ? error.message : 'Unknown error';
|
||||
return _('Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s', msg);
|
||||
}
|
||||
|
||||
async exec(method: string, path: string = '', body: any = null, headers: any = null, options: any = null): Promise<any> {
|
||||
public shareUrl(share: any): string {
|
||||
return `${this.baseUrl()}/shares/${share.id}`;
|
||||
}
|
||||
|
||||
// private requestToCurl_(url: string, options: any) {
|
||||
// const output = [];
|
||||
// output.push('curl');
|
||||
// output.push('-v');
|
||||
// if (options.method) output.push(`-X ${options.method}`);
|
||||
// if (options.headers) {
|
||||
// for (const n in options.headers) {
|
||||
// if (!options.headers.hasOwnProperty(n)) continue;
|
||||
// output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
|
||||
// }
|
||||
// }
|
||||
// if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`);
|
||||
// output.push(url);
|
||||
|
||||
// return output.join(' ');
|
||||
// }
|
||||
|
||||
public async exec(method: string, path: string = '', query: Record<string, any> = null, body: any = null, headers: any = null, options: ExecOptions = null) {
|
||||
if (headers === null) headers = {};
|
||||
if (options === null) options = {};
|
||||
if (!options.responseFormat) options.responseFormat = ExecOptionsResponseFormat.Json;
|
||||
if (!options.target) options.target = ExecOptionsTarget.String;
|
||||
|
||||
const authToken = this.authToken();
|
||||
let sessionId = '';
|
||||
if (path !== 'api/sessions' && !sessionId) {
|
||||
sessionId = await this.sessionId();
|
||||
}
|
||||
|
||||
if (authToken) headers['Authorization'] = `Basic ${authToken}`;
|
||||
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
if (typeof body === 'object' && body !== null) body = JSON.stringify(body);
|
||||
if (sessionId) headers['X-API-AUTH'] = sessionId;
|
||||
|
||||
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}`;
|
||||
if (body) {
|
||||
if (typeof body === 'object') {
|
||||
fetchOptions.body = JSON.stringify(body);
|
||||
fetchOptions.headers['Content-Type'] = 'application/json';
|
||||
} else {
|
||||
fetchOptions.body = body;
|
||||
}
|
||||
|
||||
let response = null;
|
||||
fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(fetchOptions.body)}`;
|
||||
}
|
||||
|
||||
// console.info('WebDAV Call', method + ' ' + url, headers, options);
|
||||
console.info(this.requestToCurl_(url, fetchOptions));
|
||||
let url = `${this.baseUrl()}/${path}`;
|
||||
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
if (query) {
|
||||
url += url.indexOf('?') < 0 ? '?' : '&';
|
||||
url += stringify(query);
|
||||
}
|
||||
|
||||
let response: any = null;
|
||||
|
||||
// console.info('Joplin API Call', `${method} ${url}`, headers, options);
|
||||
// console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
if (fetchOptions.path) {
|
||||
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
|
||||
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
|
||||
}
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else {
|
||||
// file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
const responseJson_: any = null;
|
||||
// console.info('Joplin API Response', responseText);
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = (message: string, code: number = 0) => {
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
|
||||
};
|
||||
|
||||
let responseJson_: any = null;
|
||||
const loadResponseJson = async () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot parse JSON: ${responseText.substr(0, 8192)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const newError = (message: string, code: number = 0) => {
|
||||
return new JoplinError(`${method} ${path}: ${message} (${code})`, code);
|
||||
responseJson_ = JSON.parse(responseText);
|
||||
if (!responseJson_) throw newError('Cannot parse JSON response', response.status);
|
||||
return responseJson_;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
if (options.target === 'file') throw newError('fetchBlob error', response.status);
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
json = await loadResponseJson();
|
||||
} catch (error) {
|
||||
throw newError(`Unknown error: ${responseText.substr(0, 8192)}`, response.status);
|
||||
// Just send back the plain text in newErro()
|
||||
}
|
||||
|
||||
const trace = json.stacktrace ? `\n${json.stacktrace}` : '';
|
||||
let message = json.error;
|
||||
if (!message) message = responseText.substr(0, 8192);
|
||||
throw newError(message + trace, response.status);
|
||||
if (json && json.error) {
|
||||
throw newError(`${json.error}`, json.code ? json.code : response.status);
|
||||
}
|
||||
|
||||
throw newError('Unknown error', response.status);
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
const output = await loadResponseJson();
|
||||
return output;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user