2019-09-08 17:16:45 +01:00
|
|
|
const { randomClipperPort } = require('./randomClipperPort');
|
2018-05-25 11:08:22 +01:00
|
|
|
|
2018-05-16 14:16:14 +01:00
|
|
|
class Bridge {
|
|
|
|
|
2019-05-11 11:18:09 +01:00
|
|
|
constructor() {
|
|
|
|
this.nounce_ = Date.now();
|
|
|
|
}
|
|
|
|
|
2020-02-11 02:49:07 -08:00
|
|
|
async init(browser, browserSupportsPromises, store) {
|
2018-05-16 14:16:14 +01:00
|
|
|
console.info('Popup: Init bridge');
|
|
|
|
|
|
|
|
this.browser_ = browser;
|
2020-02-11 02:49:07 -08:00
|
|
|
this.dispatch_ = store.dispatch;
|
|
|
|
this.store_ = store;
|
2018-05-24 18:32:30 +01:00
|
|
|
this.browserSupportsPromises_ = browserSupportsPromises;
|
2018-05-25 11:08:22 +01:00
|
|
|
this.clipperServerPort_ = null;
|
|
|
|
this.clipperServerPortStatus_ = 'searching';
|
2018-05-16 14:16:14 +01:00
|
|
|
|
2020-02-11 02:49:07 -08:00
|
|
|
function convertCommandToContent(command) {
|
|
|
|
return {
|
|
|
|
title: command.title,
|
|
|
|
body_html: command.html,
|
|
|
|
base_url: command.base_url,
|
|
|
|
source_url: command.url,
|
|
|
|
parent_id: command.parent_id,
|
|
|
|
tags: command.tags || '',
|
|
|
|
image_sizes: command.image_sizes || {},
|
|
|
|
anchor_names: command.anchor_names || [],
|
|
|
|
source_command: command.source_command,
|
|
|
|
convert_to: command.convert_to,
|
|
|
|
stylesheets: command.stylesheets,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-05-16 14:16:14 +01:00
|
|
|
this.browser_notify = async (command) => {
|
2018-06-01 15:50:11 +01:00
|
|
|
console.info('Popup: Got command:', command);
|
2019-07-30 09:35:42 +02:00
|
|
|
|
2018-05-16 14:16:14 +01:00
|
|
|
if (command.warning) {
|
2019-09-19 22:51:18 +01:00
|
|
|
console.warn(`Popup: Got warning: ${command.warning}`);
|
2018-05-16 14:16:14 +01:00
|
|
|
this.dispatch({ type: 'WARNING_SET', text: command.warning });
|
|
|
|
} else {
|
|
|
|
this.dispatch({ type: 'WARNING_SET', text: '' });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (command.name === 'clippedContent') {
|
2020-02-11 02:49:07 -08:00
|
|
|
const content = convertCommandToContent(command);
|
|
|
|
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
|
|
|
|
}
|
2018-05-20 10:19:59 +01:00
|
|
|
|
2020-02-11 02:49:07 -08:00
|
|
|
if (command.name === 'sendContentToJoplin') {
|
|
|
|
const content = convertCommandToContent(command);
|
2018-05-20 10:19:59 +01:00
|
|
|
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
|
2020-02-11 02:49:07 -08:00
|
|
|
|
|
|
|
const state = this.store_.getState();
|
|
|
|
content.parent_id = state.selectedFolderId;
|
|
|
|
if (content.parent_id) {
|
|
|
|
this.sendContentToJoplin(content);
|
|
|
|
}
|
2018-05-16 14:16:14 +01:00
|
|
|
}
|
2019-05-09 23:41:52 +01:00
|
|
|
|
|
|
|
if (command.name === 'isProbablyReaderable') {
|
|
|
|
this.dispatch({ type: 'IS_PROBABLY_READERABLE', value: command.value });
|
|
|
|
}
|
2019-07-30 09:35:42 +02:00
|
|
|
};
|
2018-05-16 14:16:14 +01:00
|
|
|
this.browser_.runtime.onMessage.addListener(this.browser_notify);
|
2018-09-22 11:21:39 +01:00
|
|
|
const backgroundPage = await this.backgroundPage(this.browser_);
|
2018-06-09 20:00:26 +01:00
|
|
|
|
|
|
|
// Not sure why the getBackgroundPage() sometimes returns null, so
|
|
|
|
// in that case default to "prod" environment, which means the live
|
|
|
|
// extension won't be affected by this bug.
|
|
|
|
this.env_ = backgroundPage ? backgroundPage.joplinEnv() : 'prod';
|
2018-06-01 15:50:11 +01:00
|
|
|
|
|
|
|
console.info('Popup: Env:', this.env());
|
|
|
|
|
|
|
|
this.dispatch({
|
|
|
|
type: 'ENV_SET',
|
|
|
|
env: this.env(),
|
|
|
|
});
|
2018-05-26 11:18:54 +01:00
|
|
|
|
2018-05-25 11:08:22 +01:00
|
|
|
this.findClipperServerPort();
|
2018-05-16 14:16:14 +01:00
|
|
|
}
|
|
|
|
|
2018-09-22 11:21:39 +01:00
|
|
|
async backgroundPage(browser) {
|
|
|
|
const bgp = browser.extension.getBackgroundPage();
|
|
|
|
if (bgp) return bgp;
|
|
|
|
|
2019-09-12 22:16:42 +00:00
|
|
|
return new Promise((resolve) => {
|
2018-09-22 11:21:39 +01:00
|
|
|
browser.runtime.getBackgroundPage((bgp) => {
|
|
|
|
resolve(bgp);
|
2019-07-30 09:35:42 +02:00
|
|
|
});
|
2018-09-22 11:21:39 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-05-26 11:18:54 +01:00
|
|
|
env() {
|
2018-06-01 15:50:11 +01:00
|
|
|
return this.env_;
|
2018-05-26 11:18:54 +01:00
|
|
|
}
|
|
|
|
|
2018-05-16 14:16:14 +01:00
|
|
|
browser() {
|
|
|
|
return this.browser_;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch(action) {
|
|
|
|
return this.dispatch_(action);
|
|
|
|
}
|
|
|
|
|
2018-05-26 15:46:57 +01:00
|
|
|
scheduleStateSave(state) {
|
|
|
|
if (this.scheduleStateSaveIID) {
|
|
|
|
clearTimeout(this.scheduleStateSaveIID);
|
|
|
|
this.scheduleStateSaveIID = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.scheduleStateSaveIID = setTimeout(() => {
|
|
|
|
this.scheduleStateSaveIID = null;
|
|
|
|
|
|
|
|
const toSave = {
|
|
|
|
selectedFolderId: state.selectedFolderId,
|
|
|
|
};
|
|
|
|
|
|
|
|
console.info('Popup: Saving state', toSave);
|
|
|
|
|
|
|
|
this.storageSet(toSave);
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
|
|
|
|
async restoreState() {
|
|
|
|
const s = await this.storageGet(null);
|
|
|
|
console.info('Popup: Restoring saved state:', s);
|
|
|
|
if (!s) return;
|
|
|
|
|
|
|
|
if (s.selectedFolderId) this.dispatch({ type: 'SELECTED_FOLDER_SET', id: s.selectedFolderId });
|
|
|
|
}
|
|
|
|
|
2018-05-25 11:08:22 +01:00
|
|
|
async findClipperServerPort() {
|
2018-05-26 12:17:41 +01:00
|
|
|
this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'searching' });
|
|
|
|
|
2018-05-25 11:08:22 +01:00
|
|
|
let state = null;
|
|
|
|
for (let i = 0; i < 10; i++) {
|
2018-05-26 11:18:54 +01:00
|
|
|
state = randomClipperPort(state, this.env());
|
2018-05-25 11:08:22 +01:00
|
|
|
|
|
|
|
try {
|
2019-09-19 22:51:18 +01:00
|
|
|
console.info(`findClipperServerPort: Trying ${state.port}`);
|
|
|
|
const response = await fetch(`http://127.0.0.1:${state.port}/ping`);
|
2018-05-25 11:08:22 +01:00
|
|
|
const text = await response.text();
|
2019-09-19 22:51:18 +01:00
|
|
|
console.info(`findClipperServerPort: Got response: ${text}`);
|
2018-05-25 11:08:22 +01:00
|
|
|
if (text.trim() === 'JoplinClipperServer') {
|
|
|
|
this.clipperServerPortStatus_ = 'found';
|
|
|
|
this.clipperServerPort_ = state.port;
|
2018-05-26 12:17:41 +01:00
|
|
|
this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'found', port: state.port });
|
2018-05-26 15:46:57 +01:00
|
|
|
|
|
|
|
const folders = await this.folderTree();
|
2020-10-27 00:37:22 +00:00
|
|
|
this.dispatch({ type: 'FOLDERS_SET', folders: folders.items ? folders.items : folders });
|
2018-09-23 18:03:11 +01:00
|
|
|
|
2020-11-25 10:27:41 +00:00
|
|
|
let tags = [];
|
|
|
|
for (let page = 1; page < 10000; page++) {
|
|
|
|
const result = await this.clipperApiExec('GET', 'tags', { page: page, order_by: 'title', order_dir: 'ASC' });
|
|
|
|
const resultTags = result.items ? result.items : result;
|
|
|
|
const hasMore = ('has_more' in result) && result.has_more;
|
|
|
|
tags = tags.concat(resultTags);
|
|
|
|
if (!hasMore) break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.dispatch({ type: 'TAGS_SET', tags: tags });
|
2018-09-23 18:44:39 +01:00
|
|
|
|
|
|
|
bridge().restoreState();
|
2018-05-25 11:08:22 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.clipperServerPortStatus_ = 'not_found';
|
|
|
|
|
2018-05-26 12:17:41 +01:00
|
|
|
this.dispatch({ type: 'CLIPPER_SERVER_SET', foundState: 'not_found' });
|
|
|
|
|
2018-05-25 11:08:22 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async clipperServerPort() {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const checkStatus = () => {
|
|
|
|
if (this.clipperServerPortStatus_ === 'not_found') {
|
|
|
|
reject(new Error('Could not find clipper service. Please make sure that Joplin is running and that the clipper server is enabled.'));
|
|
|
|
return true;
|
|
|
|
} else if (this.clipperServerPortStatus_ === 'found') {
|
|
|
|
resolve(this.clipperServerPort_);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2019-07-30 09:35:42 +02:00
|
|
|
};
|
2018-05-25 11:08:22 +01:00
|
|
|
|
|
|
|
if (checkStatus()) return;
|
|
|
|
|
2018-05-25 11:18:47 +01:00
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { searchingClipperServer: true } });
|
|
|
|
|
2018-05-25 11:08:22 +01:00
|
|
|
const waitIID = setInterval(() => {
|
|
|
|
if (!checkStatus()) return;
|
2018-05-25 11:18:47 +01:00
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: null });
|
2018-05-25 11:08:22 +01:00
|
|
|
clearInterval(waitIID);
|
|
|
|
}, 1000);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-05-25 11:18:47 +01:00
|
|
|
async clipperServerBaseUrl() {
|
|
|
|
const port = await this.clipperServerPort();
|
2019-09-19 22:51:18 +01:00
|
|
|
return `http://127.0.0.1:${port}`;
|
2018-05-25 11:18:47 +01:00
|
|
|
}
|
|
|
|
|
2018-05-24 18:32:30 +01:00
|
|
|
async tabsExecuteScript(options) {
|
|
|
|
if (this.browserSupportsPromises_) return this.browser().tabs.executeScript(options);
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.browser().tabs.executeScript(options, () => {
|
2018-06-01 15:50:11 +01:00
|
|
|
const e = this.browser().runtime.lastError;
|
|
|
|
if (e) {
|
2019-09-19 22:51:18 +01:00
|
|
|
const msg = [`tabsExecuteScript: Cannot load ${JSON.stringify(options)}`];
|
2018-06-01 15:50:11 +01:00
|
|
|
if (e.message) msg.push(e.message);
|
|
|
|
reject(new Error(msg.join(': ')));
|
|
|
|
}
|
2018-05-24 18:32:30 +01:00
|
|
|
resolve();
|
|
|
|
});
|
2019-07-30 09:35:42 +02:00
|
|
|
});
|
2018-05-24 18:32:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async tabsQuery(options) {
|
|
|
|
if (this.browserSupportsPromises_) return this.browser().tabs.query(options);
|
|
|
|
|
2019-09-12 22:16:42 +00:00
|
|
|
return new Promise((resolve) => {
|
2018-05-24 18:32:30 +01:00
|
|
|
this.browser().tabs.query(options, (tabs) => {
|
|
|
|
resolve(tabs);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async tabsSendMessage(tabId, command) {
|
|
|
|
if (this.browserSupportsPromises_) return this.browser().tabs.sendMessage(tabId, command);
|
2019-07-30 09:35:42 +02:00
|
|
|
|
2019-09-12 22:16:42 +00:00
|
|
|
return new Promise((resolve) => {
|
2018-05-24 18:32:30 +01:00
|
|
|
this.browser().tabs.sendMessage(tabId, command, (result) => {
|
|
|
|
resolve(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-05-26 12:17:41 +01:00
|
|
|
async tabsCreate(options) {
|
|
|
|
if (this.browserSupportsPromises_) return this.browser().tabs.create(options);
|
2019-07-30 09:35:42 +02:00
|
|
|
|
2019-09-12 22:16:42 +00:00
|
|
|
return new Promise((resolve) => {
|
2018-05-26 12:17:41 +01:00
|
|
|
this.browser().tabs.create(options, () => {
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-05-26 15:46:57 +01:00
|
|
|
async folderTree() {
|
2020-10-27 00:37:22 +00:00
|
|
|
return this.clipperApiExec('GET', 'folders', { as_tree: 1 });
|
2018-05-26 15:46:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async storageSet(keys) {
|
|
|
|
if (this.browserSupportsPromises_) return this.browser().storage.local.set(keys);
|
|
|
|
|
2019-09-12 22:16:42 +00:00
|
|
|
return new Promise((resolve) => {
|
2018-05-26 15:46:57 +01:00
|
|
|
this.browser().storage.local.set(keys, () => {
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async storageGet(keys, defaultValue = null) {
|
|
|
|
if (this.browserSupportsPromises_) {
|
|
|
|
try {
|
|
|
|
const r = await this.browser().storage.local.get(keys);
|
|
|
|
return r;
|
|
|
|
} catch (error) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
} else {
|
2019-09-12 22:16:42 +00:00
|
|
|
return new Promise((resolve) => {
|
2018-05-26 15:46:57 +01:00
|
|
|
this.browser().storage.local.get(keys, (result) => {
|
|
|
|
resolve(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-16 14:16:14 +01:00
|
|
|
async sendCommandToActiveTab(command) {
|
2018-05-24 18:32:30 +01:00
|
|
|
const tabs = await this.tabsQuery({ active: true, currentWindow: true });
|
2018-05-16 14:16:14 +01:00
|
|
|
if (!tabs.length) {
|
|
|
|
console.warn('No valid tab');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-05-20 10:19:59 +01:00
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: null });
|
|
|
|
|
2018-05-24 18:32:30 +01:00
|
|
|
console.info('Sending message ', command);
|
|
|
|
|
|
|
|
await this.tabsSendMessage(tabs[0].id, command);
|
2018-05-16 14:16:14 +01:00
|
|
|
}
|
|
|
|
|
2019-05-11 11:18:09 +01:00
|
|
|
async clipperApiExec(method, path, query, body) {
|
2019-09-19 22:51:18 +01:00
|
|
|
console.info(`Popup: ${method} ${path}`);
|
2018-05-26 15:46:57 +01:00
|
|
|
|
|
|
|
const baseUrl = await this.clipperServerBaseUrl();
|
|
|
|
|
|
|
|
const fetchOptions = {
|
|
|
|
method: method,
|
|
|
|
headers: {
|
2019-07-30 09:35:42 +02:00
|
|
|
'Content-Type': 'application/json',
|
2018-05-26 15:46:57 +01:00
|
|
|
},
|
2019-07-30 09:35:42 +02:00
|
|
|
};
|
2018-05-26 15:46:57 +01:00
|
|
|
|
|
|
|
if (body) fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
|
|
|
2019-05-11 11:18:09 +01:00
|
|
|
let queryString = '';
|
|
|
|
if (query) {
|
|
|
|
const s = [];
|
|
|
|
for (const k in query) {
|
|
|
|
if (!query.hasOwnProperty(k)) continue;
|
2019-09-19 22:51:18 +01:00
|
|
|
s.push(`${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`);
|
2019-05-11 11:18:09 +01:00
|
|
|
}
|
|
|
|
queryString = s.join('&');
|
2019-09-19 22:51:18 +01:00
|
|
|
if (queryString) queryString = `?${queryString}`;
|
2019-05-11 11:18:09 +01:00
|
|
|
}
|
|
|
|
|
2019-09-19 22:51:18 +01:00
|
|
|
const response = await fetch(`${baseUrl}/${path}${queryString}`, fetchOptions);
|
2018-05-26 15:46:57 +01:00
|
|
|
if (!response.ok) {
|
|
|
|
const msg = await response.text();
|
|
|
|
throw new Error(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
const json = await response.json();
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
|
2018-05-20 10:19:59 +01:00
|
|
|
async sendContentToJoplin(content) {
|
|
|
|
console.info('Popup: Sending to Joplin...');
|
|
|
|
|
|
|
|
try {
|
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: true } });
|
|
|
|
|
|
|
|
if (!content) throw new Error('Cannot send empty content');
|
|
|
|
|
2019-05-11 11:18:09 +01:00
|
|
|
// There is a bug in Chrome that somehow makes the app send the same request twice, which
|
|
|
|
// results in Joplin having the same note twice. There's a 2-3 sec delay between
|
|
|
|
// each request. The bug only happens the first time the extension popup is open and the
|
|
|
|
// Complete button is clicked.
|
|
|
|
//
|
|
|
|
// It's beyond my understanding how it's happening. I don't know how this sendContentToJoplin function
|
|
|
|
// can be called twice. But even if it is, logically, it's impossible that this
|
|
|
|
// call below would be done with twice the same nounce. Even if the function sendContentToJoplin
|
|
|
|
// is called twice in parallel, the increment is atomic and should result in two nounces
|
|
|
|
// being generated. But it's not. Somehow the function below is called twice with the exact same nounce.
|
|
|
|
//
|
|
|
|
// It's also not something internal to Chrome that repeat the request since the error is caught
|
|
|
|
// so it really seems like a double function call.
|
|
|
|
//
|
|
|
|
// So this is why below, when we get the duplicate nounce error, we just ignore it so as not to display
|
|
|
|
// a useless error message. The whole nounce feature is not for security (it's not to prevent replay
|
|
|
|
// attacks), but simply to detect these double-requests and ignore them on Joplin side.
|
|
|
|
//
|
|
|
|
// This nounce feature is optional, it's only active when the nounce query parameter is provided
|
|
|
|
// so it shouldn't affect any other call.
|
|
|
|
//
|
|
|
|
// This is the perfect Heisenbug - it happens always when opening the popup the first time EXCEPT
|
|
|
|
// when the debugger is open. Then everything is working fine and the bug NEVER EVER happens,
|
|
|
|
// so it's impossible to understand what's going on.
|
|
|
|
await this.clipperApiExec('POST', 'notes', { nounce: this.nounce_++ }, content);
|
2018-05-26 15:46:57 +01:00
|
|
|
|
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
|
2018-05-20 10:19:59 +01:00
|
|
|
} catch (error) {
|
2019-05-11 11:18:09 +01:00
|
|
|
if (error.message === '{"error":"Duplicate Nounce"}') {
|
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
|
|
|
|
} else {
|
|
|
|
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } });
|
|
|
|
}
|
2018-05-20 10:19:59 +01:00
|
|
|
}
|
|
|
|
}
|
2018-05-16 14:16:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const bridge_ = new Bridge();
|
|
|
|
|
|
|
|
const bridge = function() {
|
|
|
|
return bridge_;
|
2019-07-30 09:35:42 +02:00
|
|
|
};
|
2018-05-16 14:16:14 +01:00
|
|
|
|
2020-06-02 20:13:15 +00:00
|
|
|
// eslint-disable-next-line import/prefer-default-export
|
2019-07-30 09:35:42 +02:00
|
|
|
export { bridge };
|