1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-07-03 23:50:33 +02:00

All: Add support for application plugins (#3257)

This commit is contained in:
Laurent
2020-10-09 18:35:46 +01:00
committed by GitHub
parent 833fb1264f
commit fe41d37f8f
804 changed files with 95622 additions and 5307 deletions

View File

@ -1,3 +1,8 @@
import Setting from 'lib/models/Setting';
import Logger from 'lib/Logger';
import shim from 'lib/shim';
import uuid from 'lib/uuid';
const { ltrimSlashes } = require('lib/path-utils.js');
const { Database } = require('lib/database.js');
const Folder = require('lib/models/Folder');
@ -6,13 +11,10 @@ const Tag = require('lib/models/Tag');
const BaseItem = require('lib/models/BaseItem');
const Resource = require('lib/models/Resource');
const BaseModel = require('lib/BaseModel');
const Setting = require('lib/models/Setting');
const htmlUtils = require('lib/htmlUtils');
const markupLanguageUtils = require('lib/markupLanguageUtils');
const mimeUtils = require('lib/mime-utils.js').mime;
const { Logger } = require('lib/logger.js');
const md5 = require('md5');
const { shim } = require('lib/shim');
const HtmlToMd = require('lib/HtmlToMd');
const urlUtils = require('lib/urlUtils.js');
const ArrayUtils = require('lib/ArrayUtils.js');
@ -23,13 +25,18 @@ const SearchEngineUtils = require('lib/services/searchengine/SearchEngineUtils')
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const uri2path = require('file-uri-to-path');
const { MarkupToHtml } = require('lib/joplin-renderer');
const { uuid } = require('lib/uuid');
const { ErrorMethodNotAllowed, ErrorForbidden, ErrorBadRequest, ErrorNotFound } = require('./errors');
class Api {
constructor(token = null, actionApi = null) {
export default class Api {
private token_:string | Function;
private knownNounces_:any = {};
private logger_:Logger;
private actionApi_:any;
private htmlToMdParser_:any;
constructor(token:string = null, actionApi:any = null) {
this.token_ = token;
this.knownNounces_ = {};
this.logger_ = new Logger();
this.actionApi_ = actionApi;
}
@ -38,7 +45,7 @@ class Api {
return typeof this.token_ === 'function' ? this.token_() : this.token_;
}
parsePath(path) {
parsePath(path:string) {
path = ltrimSlashes(path);
if (!path) return { callName: '', params: [] };
@ -51,7 +58,7 @@ class Api {
};
}
async route(method, path, query = null, body = null, files = null) {
async route(method:string, path:string, query:any = null, body:any = null, files:string[] = null) {
if (!files) files = [];
if (!query) query = {};
@ -66,13 +73,13 @@ class Api {
this.knownNounces_[query.nounce] = requestMd5;
}
const request = {
const request:any = {
method: method,
path: ltrimSlashes(path),
query: query ? query : {},
body: body,
bodyJson_: null,
bodyJson: function(disallowedProperties = null) {
bodyJson: function(disallowedProperties:string[] = null) {
if (!this.bodyJson_) this.bodyJson_ = JSON.parse(this.body);
if (disallowedProperties) {
@ -104,17 +111,17 @@ class Api {
request.params = params;
if (!this[parsedPath.callName]) throw new ErrorNotFound();
if (!(this as any)[parsedPath.callName]) throw new ErrorNotFound();
try {
return await this[parsedPath.callName](request, id, link);
return await (this as any)[parsedPath.callName](request, id, link);
} catch (error) {
if (!error.httpCode) error.httpCode = 500;
throw error;
}
}
setLogger(l) {
setLogger(l:Logger) {
this.logger_ = l;
}
@ -122,23 +129,24 @@ class Api {
return this.logger_;
}
readonlyProperties(requestMethod) {
readonlyProperties(requestMethod:string) {
const output = ['created_time', 'updated_time', 'encryption_blob_encrypted', 'encryption_applied', 'encryption_cipher_text'];
if (requestMethod !== 'POST') output.splice(0, 0, 'id');
return output;
}
fields_(request, defaultFields) {
fields_(request:any, defaultFields:string[]) {
const query = request.query;
if (!query || !query.fields) return defaultFields;
if (Array.isArray(query.fields)) return query.fields.slice();
const fields = query.fields
.split(',')
.map(f => f.trim())
.filter(f => !!f);
.map((f:string) => f.trim())
.filter((f:string) => !!f);
return fields.length ? fields : defaultFields;
}
checkToken_(request) {
checkToken_(request:any) {
// For now, whitelist some calls to allow the web clipper to work
// without an extra auth step
const whiteList = [['GET', 'ping'], ['GET', 'tags'], ['GET', 'folders'], ['POST', 'notes']];
@ -152,7 +160,7 @@ class Api {
if (request.query.token !== this.token) throw new ErrorForbidden('Invalid "token" parameter');
}
async defaultAction_(modelType, request, id = null, link = null) {
async defaultAction_(modelType:number, request:any, id:string = null, link:string = null) {
this.checkToken_(request);
if (link) throw new ErrorNotFound(); // Default action doesn't support links at all for now
@ -169,7 +177,7 @@ class Api {
if (id) {
return getOneModel();
} else {
const options = {};
const options:any = {};
const fields = this.fields_(request, []);
if (fields.length) options.fields = fields;
return await ModelClass.all(options);
@ -201,7 +209,7 @@ class Api {
throw new ErrorMethodNotAllowed();
}
async action_ping(request) {
async action_ping(request:any) {
if (request.method === 'GET') {
return 'JoplinClipperServer';
}
@ -209,7 +217,7 @@ class Api {
throw new ErrorMethodNotAllowed();
}
async action_search(request) {
async action_search(request:any) {
this.checkToken_(request);
if (request.method !== 'GET') throw new ErrorMethodNotAllowed();
@ -221,7 +229,7 @@ class Api {
if (queryType !== BaseItem.TYPE_NOTE) {
const ModelClass = BaseItem.getClassByItemType(queryType);
const options = {};
const options:any = {};
const fields = this.fields_(request, []);
if (fields.length) options.fields = fields;
const sqlQueryPart = query.replace(/\*/g, '%');
@ -234,7 +242,7 @@ class Api {
}
}
async action_folders(request, id = null, link = null) {
async action_folders(request:any, id:string = null, link:string = null) {
if (request.method === 'GET' && !id) {
const folders = await FoldersScreenUtils.allForDisplay({ fields: this.fields_(request, ['id', 'parent_id', 'title']) });
const output = await Folder.allAsTree(folders);
@ -253,7 +261,7 @@ class Api {
return this.defaultAction_(BaseModel.TYPE_FOLDER, request, id, link);
}
async action_tags(request, id = null, link = null) {
async action_tags(request:any, id:string = null, link:string = null) {
if (link === 'notes') {
const tag = await Tag.load(id);
if (!tag) throw new ErrorNotFound();
@ -287,11 +295,11 @@ class Api {
return this.defaultAction_(BaseModel.TYPE_TAG, request, id, link);
}
async action_master_keys(request, id = null, link = null) {
async action_master_keys(request:any, id:string = null, link:string = null) {
return this.defaultAction_(BaseModel.TYPE_MASTER_KEY, request, id, link);
}
async action_resources(request, id = null, link = null) {
async action_resources(request:any, id:string = null, link:string = null) {
// fieldName: "data"
// headers: Object
// originalFilename: "test.jpg"
@ -327,27 +335,27 @@ class Api {
return this.defaultAction_(BaseModel.TYPE_RESOURCE, request, id, link);
}
notePreviewsOptions_(request) {
notePreviewsOptions_(request:any) {
const fields = this.fields_(request, []); // previews() already returns default fields
const options = {};
const options:any = {};
if (fields.length) options.fields = fields;
return options;
}
defaultSaveOptions_(model, requestMethod) {
const options = { userSideValidation: true };
defaultSaveOptions_(model:any, requestMethod:string) {
const options:any = { userSideValidation: true };
if (requestMethod === 'POST' && model.id) options.isNew = true;
return options;
}
defaultLoadOptions_(request) {
const options = {};
defaultLoadOptions_(request:any) {
const options:any = {};
const fields = this.fields_(request, []);
if (fields.length) options.fields = fields;
return options;
}
async execServiceActionFromRequest_(externalApi, request) {
async execServiceActionFromRequest_(externalApi:any, request:any) {
const action = externalApi[request.action];
if (!action) throw new ErrorNotFound(`Invalid action: ${request.action}`);
const args = Object.assign({}, request);
@ -355,7 +363,7 @@ class Api {
return action(args);
}
async action_services(request, serviceName) {
async action_services(request:any, serviceName:string) {
this.checkToken_(request);
if (request.method !== 'POST') throw new ErrorMethodNotAllowed();
@ -366,7 +374,7 @@ class Api {
return this.execServiceActionFromRequest_(externalApi, JSON.parse(request.body));
}
async action_notes(request, id = null, link = null) {
async action_notes(request:any, id:string = null, link:string = null) {
this.checkToken_(request);
if (request.method === 'GET') {
@ -398,17 +406,17 @@ class Api {
const requestId = Date.now();
const requestNote = JSON.parse(request.body);
const allowFileProtocolImages = urlUtils.urlProtocol(requestNote.base_url).toLowerCase() === 'file:';
// const allowFileProtocolImages = urlUtils.urlProtocol(requestNote.base_url).toLowerCase() === 'file:';
const imageSizes = requestNote.image_sizes ? requestNote.image_sizes : {};
let note = await this.requestNoteToNote_(requestNote);
let note:any = await this.requestNoteToNote_(requestNote);
const imageUrls = ArrayUtils.unique(markupLanguageUtils.extractImageUrls(note.markup_language, note.body));
this.logger().info(`Request (${requestId}): Downloading images: ${imageUrls.length}`);
let result = await this.downloadImages_(imageUrls, allowFileProtocolImages);
let result = await this.downloadImages_(imageUrls); // , allowFileProtocolImages);
this.logger().info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(result).length}`);
@ -469,8 +477,8 @@ class Api {
return this.htmlToMdParser_;
}
async requestNoteToNote_(requestNote) {
const output = {
async requestNoteToNote_(requestNote:any) {
const output:any = {
title: requestNote.title ? requestNote.title : '',
body: requestNote.body ? requestNote.body : '',
};
@ -547,19 +555,19 @@ class Api {
}
// Note must have been saved first
async attachImageFromDataUrl_(note, imageDataUrl, cropRect) {
async attachImageFromDataUrl_(note:any, imageDataUrl:string, cropRect:any) {
const tempDir = Setting.value('tempDir');
const mime = mimeUtils.fromDataUrl(imageDataUrl);
let ext = mimeUtils.toFileExtension(mime) || '';
if (ext) ext = `.${ext}`;
const tempFilePath = `${tempDir}/${md5(`${Math.random()}_${Date.now()}`)}${ext}`;
const imageConvOptions = {};
const imageConvOptions:any = {};
if (cropRect) imageConvOptions.cropRect = cropRect;
await shim.imageFromDataUrl(imageDataUrl, tempFilePath, imageConvOptions);
return await shim.attachFileToNote(note, tempFilePath);
}
async tryToGuessImageExtFromMimeType_(response, imagePath) {
async tryToGuessImageExtFromMimeType_(response:any, imagePath:string) {
const mimeType = netUtils.mimeTypeFromHeaders(response.headers);
if (!mimeType) return imagePath;
@ -571,7 +579,7 @@ class Api {
return newImagePath;
}
async buildNoteStyleSheet_(stylesheets) {
async buildNoteStyleSheet_(stylesheets:any[]) {
if (!stylesheets) return [];
const output = [];
@ -597,7 +605,7 @@ class Api {
return output;
}
async downloadImage_(url /* , allowFileProtocolImages */) {
async downloadImage_(url:string /* , allowFileProtocolImages */) {
const tempDir = Setting.value('tempDir');
const isDataUrl = url && url.toLowerCase().indexOf('data:') === 0;
@ -633,13 +641,13 @@ class Api {
}
}
async downloadImages_(urls, allowFileProtocolImages) {
async downloadImages_(urls:string[] /* , allowFileProtocolImages:boolean */) {
const PromisePool = require('es6-promise-pool');
const output = {};
const output:any = {};
const downloadOne = async url => {
const imagePath = await this.downloadImage_(url, allowFileProtocolImages);
const downloadOne = async (url:string) => {
const imagePath = await this.downloadImage_(url); // , allowFileProtocolImages);
if (imagePath) output[url] = { path: imagePath, originalUrl: url };
};
@ -658,10 +666,10 @@ class Api {
return output;
}
async createResourcesFromPaths_(urls) {
async createResourcesFromPaths_(urls:string[]) {
for (const url in urls) {
if (!urls.hasOwnProperty(url)) continue;
const urlInfo = urls[url];
const urlInfo:any = urls[url];
try {
const resource = await shim.createResourceFromPath(urlInfo.path);
urlInfo.resource = resource;
@ -672,10 +680,10 @@ class Api {
return urls;
}
async removeTempFiles_(urls) {
async removeTempFiles_(urls:string[]) {
for (const url in urls) {
if (!urls.hasOwnProperty(url)) continue;
const urlInfo = urls[url];
const urlInfo:any = urls[url];
try {
await shim.fsDriver().remove(urlInfo.path);
} catch (error) {
@ -684,18 +692,18 @@ class Api {
}
}
replaceImageUrlsByResources_(markupLanguage, md, urls, imageSizes) {
const imageSizesIndexes = {};
replaceImageUrlsByResources_(markupLanguage:number, md:string, urls:any, imageSizes:any) {
const imageSizesIndexes:any = {};
if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
return htmlUtils.replaceImageUrls(md, imageUrl => {
const urlInfo = urls[imageUrl];
return htmlUtils.replaceImageUrls(md, (imageUrl:string) => {
const urlInfo:any = urls[imageUrl];
if (!urlInfo || !urlInfo.resource) return imageUrl;
return Resource.internalUrl(urlInfo.resource);
});
} else {
// eslint-disable-next-line no-useless-escape
return md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (match, before, imageUrl, after) => {
return md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (_match:any, before:string, imageUrl:string, after:string) => {
const urlInfo = urls[imageUrl];
if (!urlInfo || !urlInfo.resource) return before + imageUrl + after;
if (!(urlInfo.originalUrl in imageSizesIndexes)) imageSizesIndexes[urlInfo.originalUrl] = 0;
@ -723,5 +731,3 @@ class Api {
}
}
}
module.exports = Api;