1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Desktop: Decrypt notes that are meant to be shared

This commit is contained in:
Laurent Cozic 2019-12-17 12:45:57 +00:00
parent f60d6e0748
commit 58200ecdb1
5 changed files with 97 additions and 13 deletions

View File

@ -1540,5 +1540,43 @@ describe('Synchronizer', function() {
expect((await synchronizer().lockFiles_()).length).toBe(0);
}));
it('should not encrypt notes that are shared', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'un', parent_id: folder1.id });
let note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await switchClient(1);
const origNote2 = Object.assign({}, note2);
await BaseItem.updateShareStatus(note2, true);
note2 = await Note.load(note2.id);
// Sharing a note should not modify the timestamps
expect(note2.user_updated_time).toBe(origNote2.user_updated_time);
expect(note2.user_created_time).toBe(origNote2.user_created_time);
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
// The shared note should be decrypted
let note2_2 = await Note.load(note2.id);
expect(note2_2.title).toBe('deux');
expect(note2_2.is_shared).toBe(1);
// The non-shared note should be encrypted
let note1_2 = await Note.load(note1.id);
expect(note1_2.title).toBe('');
}));
});

View File

@ -1,5 +1,3 @@
// const React = require('react');
// const { useState, useEffect } = React;
import * as React from 'react';
import { useState, useEffect } from 'react';
import JoplinServerApi from '../lib/JoplinServerApi';
@ -9,6 +7,7 @@ const { themeStyle, buildStyle } = require('../theme.js');
const DialogButtonRow = require('./DialogButtonRow.min');
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting');
const BaseItem = require('lib/models/BaseItem');
const { reg } = require('lib/registry.js');
const { clipboard } = require('electron');
@ -39,6 +38,7 @@ function styles_(props:ShareNoteDialogProps) {
marginBottom: 5,
},
noteTitle: {
...theme.textStyle,
flex: 1,
display: 'flex',
color: theme.color,
@ -96,24 +96,30 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
clipboard.writeText(links.join('\n'));
};
const synchronize = async () => {
const synchronizer = await reg.syncTarget().synchronizer();
await synchronizer.waitForSyncToFinish();
await reg.scheduleSync(0);
};
const shareLinkButton_click = async () => {
let hasSynced = false;
let tryToSync = false;
while (true) {
try {
setSharesState('creating');
if (tryToSync) {
const synchronizer = await reg.syncTarget().synchronizer();
await synchronizer.waitForSyncToFinish();
await reg.scheduleSync(0);
setSharesState('synchronizing');
await synchronize();
tryToSync = false;
hasSynced = true;
}
setSharesState('creating');
const api = await appApi();
const syncTargetId = api.syncTargetId(Setting.toPlainObject());
const newShares = Object.assign({}, shares);
let sharedStatusChanged = false;
for (const note of notes) {
const result = await api.exec('POST', 'shares', {
@ -121,10 +127,19 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
noteId: note.id,
});
newShares[note.id] = result;
const changed = await BaseItem.updateShareStatus(note, true);
if (changed) sharedStatusChanged = true;
}
setShares(newShares);
if (sharedStatusChanged) {
setSharesState('synchronizing');
await synchronize();
setSharesState('creating');
}
copyLinksToClipboard(newShares);
setSharesState('created');
@ -178,11 +193,14 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
};
const statusMessage = (sharesState:string):string => {
if (sharesState === 'synchronizing') return _('Synchronising...');
if (sharesState === 'creating') return _n('Generating link...', 'Generating links...', noteCount);
if (sharesState === 'created') return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
return '';
};
const encryptionWarningMessage = !Setting.value('encryption.enabled') ? null : <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}</div>;
const rootStyle = Object.assign({}, theme.dialogBox);
rootStyle.width = '50%';
@ -191,7 +209,8 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
<div style={rootStyle}>
<div style={theme.dialogTitle}>{_('Share Notes')}</div>
{renderNoteList(notes)}
<button disabled={sharesState === 'creating'} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
{encryptionWarningMessage}
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>

View File

@ -7,9 +7,9 @@ xcopy /C /I /H /R /Y /S /Q %script_dir%\..\ReactNativeClient\lib %script_dir%\ap
REM if %errorlevel% neq 0 exit /b %errorlevel%
rem Note that TypeScript must be installed globally for this to work
cd %script_dir%\..
call tsc
if %errorlevel% neq 0 exit /b %errorlevel%
REM cd %script_dir%\..
REM call tsc
REM if %errorlevel% neq 0 exit /b %errorlevel%
cd %script_dir%\app
call npm run compile

View File

@ -302,7 +302,7 @@ class JoplinDatabase extends Database {
// must be set in the synchronizer too.
// Note: v16 and v17 don't do anything. They were used to debug an issue.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
@ -649,6 +649,14 @@ class JoplinDatabase extends Database {
GROUP BY tags.id`);
}
if (targetVersion == 26) {
const tableNames = ['notes', 'folders', 'tags', 'note_tags', 'resources'];
for (let i = 0; i < tableNames.length; i++) {
const n = tableNames[i];
queries.push(`ALTER TABLE ${n} ADD COLUMN is_shared INT NOT NULL DEFAULT 0`);
}
}
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
try {

View File

@ -329,7 +329,7 @@ class BaseItem extends BaseModel {
const serialized = await ItemClass.serialize(item, shownKeys);
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) {
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported() || item.is_shared) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
if (item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted');
return serialized;
@ -716,6 +716,25 @@ class BaseItem extends BaseModel {
}
}
static async updateShareStatus(item, isShared) {
if (!item.id || !item.type_) throw new Error('Item must have an ID and a type');
if (!!item.is_shared === !!isShared) return false;
const ItemClass = this.getClassByItemType(item.type_);
// No auto-timestamp because sharing a note is not seen as an update
await ItemClass.save({
id: item.id,
is_shared: isShared ? 1 : 0,
updated_time: Date.now(),
}, { autoTimestamp: false });
// The timestamps have not been changed but still need the note to be synced
// so we force-sync it.
// await this.forceSync(item.id);
return true;
}
static async forceSync(itemId) {
await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]);
}