async function setupDatabase(id = null) { if (id === null) id = currentClient_; Setting.cancelScheduleSave(); Setting.cache_ = null; if (databases_[id]) { await clearDatabase(id); await Setting.load(); return; } const filePath = __dirname + '/data/test-' + id + '.sqlite'; try { await fs.unlink(filePath); } catch (error) { // Don't care if the file doesn't exist }; databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); await databases_[id].open({ name: filePath }); BaseModel.db_ = databases_[id]; await Setting.load(); }
async refreshNotes(state) { let parentType = state.notesParentType; let parentId = null; if (parentType === 'Folder') { parentId = state.selectedFolderId; parentType = BaseModel.TYPE_FOLDER; } else if (parentType === 'Tag') { parentId = state.selectedTagId; parentType = BaseModel.TYPE_TAG; } else if (parentType === 'Search') { parentId = state.selectedSearchId; parentType = BaseModel.TYPE_SEARCH; } this.logger().debug('Refreshing notes:', parentType, parentId); let options = { order: stateUtils.notesOrder(state.settings), uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'), showCompletedTodos: Setting.value('showCompletedTodos'), caseInsensitive: true, }; const source = JSON.stringify({ options: options, parentId: parentId, }); let notes = []; if (parentId) { if (parentType === Folder.modelType()) { notes = await Note.previews(parentId, options); } else if (parentType === Tag.modelType()) { notes = await Tag.notes(parentId, options); } else if (parentType === BaseModel.TYPE_SEARCH) { let fields = Note.previewFields(); let search = BaseModel.byId(state.searches, parentId); notes = await Note.previews(null, { fields: fields, anywherePattern: '*' + search.query_pattern + '*', }); } } this.store().dispatch({ type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: source, }); this.store().dispatch({ type: 'NOTE_SELECT', id: notes.length ? notes[0].id : null, }); }
async generalMiddleware(store, next, action) { if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') { setLocale(Setting.value('locale')); // The bridge runs within the main process, with its own instance of locale.js // so it needs to be set too here. bridge().setLocale(Setting.value('locale')); this.refreshMenu(); } if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') { this.updateTray(); } if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'style.editor.fontFamily' || action.type == 'SETTING_UPDATE_ALL') { this.updateEditorFont(); } if (["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) { if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(30 * 1000, { syncSteps: ["update_remote", "delete_remote"] }); } if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) { await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE'); } const result = await super.generalMiddleware(store, next, action); const newState = store.getState(); if (action.type === 'NAV_GO' || action.type === 'NAV_BACK') { app().updateMenu(newState.route.routeName); } if (['NOTE_VISIBLE_PANES_TOGGLE', 'NOTE_VISIBLE_PANES_SET'].indexOf(action.type) >= 0) { Setting.setValue('noteVisiblePanes', newState.noteVisiblePanes); } if (['SIDEBAR_VISIBILITY_TOGGLE', 'SIDEBAR_VISIBILITY_SET'].indexOf(action.type) >= 0) { Setting.setValue('sidebarVisibility', newState.sidebarVisibility); } if (action.type === 'SYNC_STARTED') { if (!this.powerSaveBlockerId_) this.powerSaveBlockerId_ = bridge().powerSaveBlockerStart('prevent-app-suspension'); } if (action.type === 'SYNC_COMPLETED') { if (this.powerSaveBlockerId_) { bridge().powerSaveBlockerStop(this.powerSaveBlockerId_); this.powerSaveBlockerId_ = null; } } return result; }
const renderKeyValue = (name) => { const md = Setting.settingMetadata(name); let value = Setting.value(name); if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value); if (md.secure) value = '********'; if (Setting.isEnum(name)) { return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name)); } else { return _('%s = %s', name, value); } }
testUnits.testConfig = async () => { await execCommand(client, 'config editor vim'); await Setting.load(); assertEquals('vim', Setting.value('editor')); await execCommand(client, 'config editor subl'); await Setting.load(); assertEquals('subl', Setting.value('editor')); let r = await execCommand(client, 'config'); assertTrue(r.indexOf('editor') >= 0); assertTrue(r.indexOf('subl') >= 0); }
updateEditorFont() { const fontFamilies = []; if (Setting.value('style.editor.fontFamily')) fontFamilies.push('"' + Setting.value('style.editor.fontFamily') + '"'); fontFamilies.push('monospace'); // The '*' and '!important' parts are necessary to make sure Russian text is displayed properly // https://github.com/laurent22/joplin/issues/155 const css = '.ace_editor * { font-family: ' + fontFamilies.join(', ') + ' !important; }'; const styleTag = document.createElement('style'); styleTag.type = 'text/css'; styleTag.appendChild(document.createTextNode(css)); document.head.appendChild(styleTag); }
async enableEncryption(masterKey, password = null) { Setting.setValue('encryption.enabled', true); Setting.setValue('encryption.activeMasterKeyId', masterKey.id); if (password) { let passwordCache = Setting.value('encryption.passwordCache'); passwordCache[masterKey.id] = password; Setting.setValue('encryption.passwordCache', passwordCache); } // Mark only the non-encrypted ones for sync since, if there are encrypted ones, // it means they come from the sync target and are already encrypted over there. await BaseItem.markAllNonEncryptedForSync(); }
updateTray() { const app = bridge().electronApp(); if (app.trayShown() === Setting.value('showTrayIcon')) return; if (!Setting.value('showTrayIcon')) { app.destroyTray(); } else { const contextMenu = Menu.buildFromTemplate([ { label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } }, { type: 'separator' }, { label: _('Exit'), click: () => { app.quit() } }, ]) app.createTray(contextMenu); } }
// Prepare the resource by encrypting it if needed. // The call returns the path to the physical file AND a representation of the resource object // as it should be uploaded to the sync target. Note that this may be different from what is stored // in the database. In particular, the flag encryption_blob_encrypted might be 1 on the sync target // if the resource is encrypted, but will be 0 locally because the device has the decrypted resource. static async fullPathForSyncUpload(resource) { const plainTextPath = this.fullPath(resource); if (!Setting.value('encryption.enabled')) { // Normally not possible since itemsThatNeedSync should only return decrypted items if (!!resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled'); return { path: plainTextPath, resource: resource }; } const encryptedPath = this.fullPath(resource, true); if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource }; try { // const stat = await this.fsDriver().stat(plainTextPath); await this.encryptionService().encryptFile(plainTextPath, encryptedPath, { // onProgress: (progress) => { // console.info(progress.doneSize / stat.size); // }, }); } catch (error) { if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound'); throw error; } const resourceCopy = Object.assign({}, resource); resourceCopy.encryption_blob_encrypted = 1; return { path: encryptedPath, resource: resourceCopy }; }
determineProfileDir(initArgs) { if (initArgs.profileDir) return initArgs.profileDir; if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) return process.env.PORTABLE_EXECUTABLE_DIR + '/JoplinProfile'; return os.homedir() + '/.config/' + Setting.value('appName'); }
shared.checkSyncConfig = async function(comp, settings) { const syncTargetId = settings['sync.target']; const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId); const options = Setting.subValues('sync.' + syncTargetId, settings); comp.setState({ checkSyncConfigResult: 'checking' }); const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options)); comp.setState({ checkSyncConfigResult: result }); }
async action(args) { const verbose = args.options.verbose; const renderKeyValue = (name) => { const md = Setting.settingMetadata(name); let value = Setting.value(name); if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value); if (md.secure) value = '********'; if (Setting.isEnum(name)) { return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name)); } else { return _('%s = %s', name, value); } } if (!args.name && !args.value) { let keys = Setting.keys(!verbose, 'cli'); keys.sort(); for (let i = 0; i < keys.length; i++) { const value = Setting.value(keys[i]); if (!verbose && !value) continue; this.stdout(renderKeyValue(keys[i])); } app().gui().showConsole(); app().gui().maximizeConsole(); return; } if (args.name && !args.value) { this.stdout(renderKeyValue(args.name)); app().gui().showConsole(); app().gui().maximizeConsole(); return; } Setting.setValue(args.name, args.value); if (args.name == 'locale') { setLocale(Setting.value('locale')); app().onLocaleChanged(); } await Setting.saveAll(); }
async function switchClient(id) { await time.msleep(sleepTime); // Always leave a little time so that updated_time properties don't overlap await Setting.saveAll(); currentClient_ = id; BaseModel.db_ = databases_[id]; Folder.db_ = databases_[id]; Note.db_ = databases_[id]; BaseItem.db_ = databases_[id]; Setting.db_ = databases_[id]; BaseItem.encryptionService_ = encryptionServices_[id]; Resource.encryptionService_ = encryptionServices_[id]; Setting.setConstant('resourceDir', resourceDir(id)); return Setting.load(); }
async initFileApi() { const syncPath = Setting.value('sync.2.path'); const driver = new FileApiDriverLocal(); const fileApi = new FileApi(syncPath, driver); fileApi.setLogger(this.logger()); fileApi.setSyncTargetId(SyncTargetFilesystem.id()); await driver.mkdir(syncPath); return fileApi; }
shared.settingsToComponents = function(comp, device, settings) { const keys = Setting.keys(true, device); const settingComps = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (!Setting.isPublic(key)) continue; const md = Setting.settingMetadata(key); if (md.show && !md.show(settings)) continue; const settingComp = comp.settingToComponent(key, settings[key]); if (!settingComp) continue; settingComps.push(settingComp); } return settingComps }
shared.saveSettings = function(comp) { for (let key in comp.state.settings) { if (!comp.state.settings.hasOwnProperty(key)) continue; if (comp.state.changedSettingKeys.indexOf(key) < 0) continue; console.info("Saving", key, comp.state.settings[key]); Setting.setValue(key, comp.state.settings[key]); } comp.setState({ changedSettingKeys: [] }); }
switchCurrentFolder(folder) { if (!this.hasGui()) { this.currentFolder_ = Object.assign({}, folder); Setting.setValue('activeFolderId', folder ? folder.id : ''); } else { this.dispatch({ type: 'FOLDER_SELECT', id: folder ? folder.id : '', }); } }
shared.updateSettingValue = function(comp, key, value) { const settings = Object.assign({}, comp.state.settings); const changedSettingKeys = comp.state.changedSettingKeys.slice(); settings[key] = Setting.formatValue(key, value); if (changedSettingKeys.indexOf(key) < 0) changedSettingKeys.push(key); comp.setState({ settings: settings, changedSettingKeys: changedSettingKeys, }); }
click: () => { const p = packageInfo; let message = [ p.description, '', 'Copyright © 2016-2018 Laurent Cozic', _('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform), ]; bridge().showInfoMessageBox(message.join('\n'), { icon: bridge().electronApp().buildDir() + '/icons/32x32.png', }); }
async loadMasterKeysFromSettings() { const masterKeys = await MasterKey.all(); const passwords = Setting.value('encryption.passwordCache'); const activeMasterKeyId = Setting.value('encryption.activeMasterKeyId'); this.logger().info('Trying to load ' + masterKeys.length + ' master keys...'); for (let i = 0; i < masterKeys.length; i++) { const mk = masterKeys[i]; const password = passwords[mk.id]; if (this.isMasterKeyLoaded(mk.id)) continue; if (!password) continue; try { await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id); } catch (error) { this.logger().warn('Cannot load master key ' + mk.id + '. Invalid password?', error); } } this.logger().info('Loaded master keys: ' + this.loadedMasterKeysCount()); }
async disableEncryption() { // Allow disabling encryption even if some items are still encrypted, because whether E2EE is enabled or disabled // should not affect whether items will enventually be decrypted or not (DecryptionWorker will still work as // long as there are encrypted items). Also even if decryption is disabled, it's possible that encrypted items // will still be received via synchronisation. // const hasEncryptedItems = await BaseItem.hasEncryptedItems(); // if (hasEncryptedItems) throw new Error(_('Encryption cannot currently be disabled because some items are still encrypted. Please wait for all the items to be decrypted and try again.')); Setting.setValue('encryption.enabled', false); // The only way to make sure everything gets decrypted on the sync target is // to re-sync everything. await BaseItem.forceSyncAll(); }
static currentPosition(options = null) { if (Setting.value('env') == 'dev') return this.currentPosition_testResponse(); if (!options) options = {}; if (!('enableHighAccuracy' in options)) options.enableHighAccuracy = true; if (!('timeout' in options)) options.timeout = 10000; return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition((data) => { resolve(data); }, (error) => { reject(error); }, options); }); }
async generateMasterKey(password) { const bytes = await shim.randomBytes(256); const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join(''); const checksum = this.sha256(hexaBytes); const encryptionMethod = EncryptionService.METHOD_SJCL_2; const cipherText = await this.encrypt(encryptionMethod, password, hexaBytes); const now = Date.now(); return { created_time: now, updated_time: now, source_application: Setting.value('appId'), encryption_method: encryptionMethod, checksum: checksum, content: cipherText, }; }
async function main(argv) { await fs.remove(baseDir); logger.info(await execCommand(client, 'version')); await db.open({ name: client.profileDir + '/database.sqlite' }); BaseModel.db_ = db; await Setting.load(); let onlyThisTest = 'testMv'; onlyThisTest = ''; for (let n in testUnits) { if (!testUnits.hasOwnProperty(n)) continue; if (onlyThisTest && n != onlyThisTest) continue; await clearDatabase(); let testName = n.substr(4).toLowerCase(); process.stdout.write(testName + ': '); await testUnits[n](); console.info(''); } }
const runAutoUpdateCheck = () => { if (Setting.value('autoUpdateEnabled')) { bridge().checkForUpdates(true, bridge().window(), this.checkForUpdateLoggerPath()); } }
async exit(code = 0) { await Setting.saveAll(); process.exit(code); }
// Handles the initial flags passed to main script and // returns the remaining args. async handleStartFlags_(argv, setDefaults = true) { let matched = {}; argv = argv.slice(0); argv.splice(0, 2); // First arguments are the node executable, and the node JS file while (argv.length) { let arg = argv[0]; let nextArg = argv.length >= 2 ? argv[1] : null; if (arg == '--profile') { if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError'); matched.profileDir = nextArg; argv.splice(0, 2); continue; } if (arg == '--env') { if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError'); matched.env = nextArg; argv.splice(0, 2); continue; } if (arg == '--is-demo') { Setting.setConstant('isDemo', true); argv.splice(0, 1); continue; } if (arg == '--open-dev-tools') { Setting.setConstant('openDevTools', true); argv.splice(0, 1); continue; } if (arg == '--update-geolocation-disabled') { Note.updateGeolocationEnabled_ = false; argv.splice(0, 1); continue; } if (arg == '--stack-trace-enabled') { this.showStackTraces_ = true; argv.splice(0, 1); continue; } if (arg == '--log-level') { if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError'); matched.logLevel = Logger.levelStringToId(nextArg); argv.splice(0, 2); continue; } if (arg.indexOf('-psn') === 0) { // Some weird flag passed by macOS - can be ignored. // https://github.com/laurent22/joplin/issues/480 // https://stackoverflow.com/questions/10242115 argv.splice(0, 1); continue; } if (arg.length && arg[0] == '-') { throw new JoplinError(_('Unknown flag: %s', arg), 'flagError'); } else { break; } } if (setDefaults) { if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO; if (!matched.env) matched.env = 'prod'; } return { matched: matched, argv: argv, }; }
async generalMiddleware(store, next, action) { this.logger().debug('Reducer action', this.reducerActionToString(action)); const result = next(action); const newState = store.getState(); let refreshNotes = false; reduxSharedMiddleware(store, next, action); if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) { Setting.setValue('activeFolderId', newState.selectedFolderId); this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null; refreshNotes = true; } if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'uncompletedTodosOnTop') || action.type == 'SETTING_UPDATE_ALL')) { refreshNotes = true; } if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'showCompletedTodos') || action.type == 'SETTING_UPDATE_ALL')) { refreshNotes = true; } if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) { refreshNotes = true; } if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') { refreshNotes = true; } if (action.type == 'SEARCH_SELECT' || action.type === 'SEARCH_DELETE') { refreshNotes = true; } if (refreshNotes) { await this.refreshNotes(newState); } if ((action.type == 'SETTING_UPDATE_ONE' && (action.key == 'dateFormat' || action.key == 'timeFormat')) || (action.type == 'SETTING_UPDATE_ALL')) { time.setDateFormat(Setting.value('dateFormat')); time.setTimeFormat(Setting.value('timeFormat')); } if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.ignoreTlsErrors') || (action.type == 'SETTING_UPDATE_ALL')) { // https://stackoverflow.com/questions/20082893/unable-to-verify-leaf-signature process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = Setting.value('net.ignoreTlsErrors') ? '0' : '1'; } if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.customCertificates') || (action.type == 'SETTING_UPDATE_ALL')) { const caPaths = Setting.value('net.customCertificates').split(','); for (let i = 0; i < caPaths.length; i++) { const f = caPaths[i].trim(); if (!f) continue; syswidecas.addCAs(f); } } if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) { if (this.hasGui()) { await EncryptionService.instance().loadMasterKeysFromSettings(); DecryptionWorker.instance().scheduleStart(); const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds(); this.dispatch({ type: 'MASTERKEY_REMOVE_NOT_LOADED', ids: loadedMasterKeyIds, }); // Schedule a sync operation so that items that need to be encrypted // are sent to sync target. reg.scheduleSync(); } } if (action.type === 'NOTE_UPDATE_ONE') { // If there is a conflict, we refresh the folders so as to display "Conflicts" folder if (action.note && action.note.is_conflict) { await FoldersScreenUtils.refreshFolders(); } } if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') { reg.setupRecurrentSync(); } if (this.hasGui() && action.type === 'SYNC_GOT_ENCRYPTED_ITEM') { DecryptionWorker.instance().scheduleStart(); } return result; }
EncryptionService.instance().randomHexString(64).then((token) => { Setting.setValue('api.token', token); });
async start(argv) { let startFlags = await this.handleStartFlags_(argv); argv = startFlags.argv; let initArgs = startFlags.matched; if (argv.length) this.showPromptString_ = false; let appName = initArgs.env == 'dev' ? 'joplindev' : 'joplin'; if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop'; Setting.setConstant('appName', appName); const profileDir = this.determineProfileDir(initArgs); const resourceDir = profileDir + '/resources'; const tempDir = profileDir + '/tmp'; Setting.setConstant('env', initArgs.env); Setting.setConstant('profileDir', profileDir); Setting.setConstant('resourceDir', resourceDir); Setting.setConstant('tempDir', tempDir); await shim.fsDriver().remove(tempDir); await fs.mkdirp(profileDir, 0o755); await fs.mkdirp(resourceDir, 0o755); await fs.mkdirp(tempDir, 0o755); const extraFlags = await this.readFlagsFromFile(profileDir + '/flags.txt'); initArgs = Object.assign(initArgs, extraFlags); this.logger_.addTarget('file', { path: profileDir + '/log.txt' }); //this.logger_.addTarget('console'); this.logger_.setLevel(initArgs.logLevel); reg.setLogger(this.logger_); reg.dispatch = (o) => {}; this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' }); this.dbLogger_.setLevel(initArgs.logLevel); if (Setting.value('env') === 'dev') { this.dbLogger_.setLevel(Logger.LEVEL_WARN); } this.logger_.info('Profile directory: ' + profileDir); this.database_ = new JoplinDatabase(new DatabaseDriverNode()); this.database_.setLogExcludedQueryTypes(['SELECT']); this.database_.setLogger(this.dbLogger_); await this.database_.open({ name: profileDir + '/database.sqlite' }); reg.setDb(this.database_); BaseModel.db_ = this.database_; await Setting.load(); if (Setting.value('firstStart')) { const locale = shim.detectAndSetLocale(Setting); reg.logger().info('First start: detected locale as ' + locale); if (Setting.value('env') === 'dev') { Setting.setValue('showTrayIcon', 0); Setting.setValue('autoUpdateEnabled', 0); Setting.setValue('sync.interval', 3600); } Setting.setValue('firstStart', 0); } else { setLocale(Setting.value('locale')); } if (!Setting.value('api.token')) { EncryptionService.instance().randomHexString(64).then((token) => { Setting.setValue('api.token', token); }); } time.setDateFormat(Setting.value('dateFormat')); time.setTimeFormat(Setting.value('timeFormat')); BaseService.logger_ = this.logger_; EncryptionService.instance().setLogger(this.logger_); BaseItem.encryptionService_ = EncryptionService.instance(); DecryptionWorker.instance().setLogger(this.logger_); DecryptionWorker.instance().setEncryptionService(EncryptionService.instance()); await EncryptionService.instance().loadMasterKeysFromSettings(); let currentFolderId = Setting.value('activeFolderId'); let currentFolder = null; if (currentFolderId) currentFolder = await Folder.load(currentFolderId); if (!currentFolder) currentFolder = await Folder.defaultFolder(); Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : ''); // await this.testing();process.exit(); return argv; }