1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-30 10:36:35 +02:00

Merge branch 'dev' into release-2.4

This commit is contained in:
Laurent Cozic 2021-09-14 12:07:04 +01:00
commit f6f5d6808d
50 changed files with 1485 additions and 1552 deletions

View File

@ -208,9 +208,9 @@ packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
@ -931,9 +931,9 @@ packages/lib/commands/index.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/components/shared/encryption-config-shared.d.ts
packages/lib/components/shared/encryption-config-shared.js
packages/lib/components/shared/encryption-config-shared.js.map
packages/lib/components/EncryptionConfigScreen/utils.d.ts
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/EncryptionConfigScreen/utils.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map

12
.gitignore vendored
View File

@ -193,9 +193,9 @@ packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
@ -916,9 +916,9 @@ packages/lib/commands/index.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/components/shared/encryption-config-shared.d.ts
packages/lib/components/shared/encryption-config-shared.js
packages/lib/components/shared/encryption-config-shared.js.map
packages/lib/components/EncryptionConfigScreen/utils.d.ts
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/EncryptionConfigScreen/utils.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map

View File

@ -6,7 +6,7 @@ import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/plugins/PluginsStates';
import EncryptionConfigScreen from '../EncryptionConfigScreen';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');

View File

@ -1,394 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import { State } from '@joplin/lib/reducer';
import shim from '@joplin/lib/shim';
import dialogs from './dialogs';
import bridge from '../services/bridge';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import MasterKey from '@joplin/lib/models/MasterKey';
import StyledInput from './style/StyledInput';
import Button, { ButtonLevel } from './Button/Button';
import styled from 'styled-components';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
interface Props {}
class EncryptionConfigScreenComponent extends React.Component<Props> {
constructor(props: Props) {
super(props);
shared.initialize(this, props);
}
componentWillUnmount() {
this.isMounted_ = false;
shared.componentWillUnmount();
}
componentDidMount() {
this.isMounted_ = true;
shared.componentDidMount(this);
}
componentDidUpdate(prevProps: Props) {
shared.componentDidUpdate(this, prevProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
private renderMasterKey(mk: MasterKeyEntity, _isDefault: boolean) {
const theme = themeStyle(this.props.themeId);
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
};
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
};
const onPasswordChange = (event: any) => {
return shared.onPasswordChange(this, mk, event.target.value);
};
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
);
}
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const isActive = this.props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
}
renderNeedUpgradeSection() {
if (!shim.isElectron()) return null;
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const rows = [];
const comp = this;
for (const mk of needUpgradeMasterKeys) {
rows.push(
<tr key={mk.id}>
<td style={theme.textStyle}>{mk.id}</td>
<td><button onClick={() => shared.upgradeMasterKey(comp, mk)} style={theme.buttonStyle}>Upgrade</button></td>
</tr>
);
}
return (
<div>
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Upgrade')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
renderReencryptData() {
if (!shim.isElectron()) return null;
if (!this.props.shouldReencrypt) return null;
const theme = themeStyle(this.props.themeId);
const buttonLabel = _('Re-encrypt data');
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
t = t.replace(/\n\n/g, '</p><p>');
t = t.replace(/\n/g, '<br>');
t = `<p>${t}</p>`;
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => shared.reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
</span>
{ !this.props.shouldReencrypt ? null : <button onClick={() => shared.dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
);
}
private renderMasterKeySection(masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) {
const theme = themeStyle(this.props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || this.state.showDisabledMasterKeys;
const latestMasterKey = MasterKey.latest();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk, isEnabledMasterKeys && latestMasterKey && mk.id === latestMasterKey.id));
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
return null;
}
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
if (this.state.passwordChecks['master']) {
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
overflow: 'auto',
backgroundColor: theme.backgroundColor3,
});
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
const masterKey = getDefaultMasterKey();
let answer = null;
if (isEnabled) {
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
} else {
const msg = shared.enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
} catch (error) {
await dialogs.alert(error.message);
}
};
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
const toggleButton = (
<button
style={theme.buttonStyle}
onClick={() => {
void onToggleButtonClick();
}}
>
{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
</button>
);
const needUpgradeSection = this.renderNeedUpgradeSection();
const reencryptDataSection = this.renderReencryptData();
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<tr key={id}>
<td style={theme.textStyle}>{id}</td>
</tr>
);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
return (
<div>
<div style={containerStyle}>
{
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
<p style={theme.textStyle}>
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
<a
onClick={() => {
bridge().openExternal('https://joplinapp.org/e2ee/');
}}
href="#"
style={theme.urlStyle}
>
https://joplinapp.org/e2ee/
</a>
</p>
</div>
}
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{this.renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{this.props.shouldReencrypt ? reencryptDataSection : null}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!this.props.shouldReencrypt ? reencryptDataSection : null}
</div>
</div>
);
}
}
const mapStateToProps = (state: State) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
themeId: state.settings.theme,
masterKeys: syncInfo.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
export default EncryptionConfigScreen;

View File

@ -0,0 +1,366 @@
const React = require('react');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import dialogs from '../dialogs';
import bridge from '../../services/bridge';
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import StyledInput from '../style/StyledInput';
import Button, { ButtonLevel } from '../Button/Button';
import styled from 'styled-components';
import { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
interface Props {
themeId: any;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
}
const EncryptionConfigScreen = (props: Props) => {
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const theme: any = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
void upgradeMasterKey(mk, passwordChecks, props.passwords);
}, [passwordChecks, props.passwords]);
const renderNeedUpgradeSection = () => {
if (!shim.isElectron()) return null;
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(props.masterKeys);
if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(props.themeId);
const rows = [];
for (const mk of needUpgradeMasterKeys) {
rows.push(
<tr key={mk.id}>
<td style={theme.textStyle}>{mk.id}</td>
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
</tr>
);
}
return (
<div>
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Upgrade')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
};
const renderReencryptData = () => {
if (!shim.isElectron()) return null;
if (!props.shouldReencrypt) return null;
const theme = themeStyle(props.themeId);
const buttonLabel = _('Re-encrypt data');
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
t = t.replace(/\n\n/g, '</p><p>');
t = t.replace(/\n/g, '<br>');
t = `<p>${t}</p>`;
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
</span>
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
);
};
const renderMasterKey = (mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
};
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const isActive = props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
const renderPasswordInput = (masterKeyId: string) => {
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onInputPasswordChange(mk, event.target.value)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, props.passwords)}>
{_('Save')}
</button>
</td>
);
}
};
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
};
const renderMasterKeySection = (masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) => {
const theme = themeStyle(props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || showDisabledMasterKeys;
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(renderMasterKey(mk));
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
return null;
};
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const renderMasterPassword = () => {
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
const theme = themeStyle(props.themeId);
if (passwordChecks['master']) {
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={inputMasterPassword} onChange={(event: any) => onMasterPasswordChange(event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
};
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
overflow: 'auto',
backgroundColor: theme.backgroundColor3,
});
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
const masterKey = getDefaultMasterKey();
let answer = null;
if (isEnabled) {
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
} else {
const msg = enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
} catch (error) {
await dialogs.alert(error.message);
}
};
const decryptedItemsInfo = <p style={theme.textStyle}>{decryptedStatText(stats)}</p>;
const toggleButton = (
<button
style={theme.buttonStyle}
onClick={() => {
void onToggleButtonClick();
}}
>
{props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
</button>
);
const needUpgradeSection = renderNeedUpgradeSection();
const reencryptDataSection = renderReencryptData();
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<tr key={id}>
<td style={theme.textStyle}>{id}</td>
</tr>
);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
return (
<div>
<div style={containerStyle}>
{
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
<p style={theme.textStyle}>
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
<a
onClick={() => {
bridge().openExternal('https://joplinapp.org/e2ee/');
}}
href="#"
style={theme.urlStyle}
>
https://joplinapp.org/e2ee/
</a>
</p>
</div>
}
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{props.shouldReencrypt ? reencryptDataSection : null}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!props.shouldReencrypt ? reencryptDataSection : null}
</div>
</div>
);
};
const mapStateToProps = (state: AppState) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
themeId: state.settings.theme,
masterKeys: syncInfo.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};
export default connect(mapStateToProps)(EncryptionConfigScreen);

View File

@ -0,0 +1,5 @@
.encryption-config-test {
& > .item {
font-weight: bold;
}
}

View File

@ -1,5 +1,6 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
const compileSass = require('@joplin/tools/compileSass');
const tasks = {
compileScripts: {
@ -20,6 +21,14 @@ const tasks = {
tsc: require('@joplin/tools/gulp/tasks/tsc'),
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
compileSass: {
fn: async () => {
const guiDir = `${__dirname}/gui`;
await compileSass([
`${guiDir}/EncryptionConfigScreen/style.scss`,
], `${__dirname}/style.min.css`);
},
},
};
utils.registerGulpTasks(gulp, tasks);
@ -31,6 +40,7 @@ const buildParallel = [
'copyTinyMceLangs',
'updateIgnoredTypeScriptBuild',
'buildCommandIndex',
'compileSass',
];
gulp.task('build', gulp.parallel(...buildParallel));

5
packages/app-desktop/style.min.css vendored Normal file
View File

@ -0,0 +1,5 @@
.encryption-config-test > .item {
font-weight: bold;
}
/*# sourceMappingURL=style.min.css.map */

View File

@ -2,68 +2,56 @@ const React = require('react');
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { State } from '@joplin/lib/reducer';
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import { useMemo, useRef, useState } from 'react';
interface Props {}
interface Props {
themeId: any;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
}
class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
static navigationOptions(): any {
return { header: null };
}
const EncryptionConfigScreen = (props: Props) => {
const [passwordPromptShow, setPasswordPromptShow] = useState(false);
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const dialogBoxRef = useRef(null);
constructor(props: Props) {
super(props);
const mkComps = [];
this.state = {
passwordPromptShow: false,
passwordPromptAnswer: '',
passwordPromptConfirmAnswer: '',
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
const theme: any = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const rootStyle = useMemo(() => {
return {
flex: 1,
backgroundColor: theme.backgroundColor,
};
}, [theme]);
shared.initialize(this, props);
this.styles_ = {};
}
componentWillUnmount() {
this.isMounted_ = false;
}
async refreshStats() {
return shared.refreshStats(this);
}
componentDidMount() {
this.isMounted_ = true;
shared.componentDidMount(this);
}
componentDidUpdate(prevProps: Props) {
shared.componentDidUpdate(this, prevProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
styles() {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
const styles = useMemo(() => {
const styles = {
titleText: {
flex: 1,
@ -93,39 +81,32 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
},
};
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
}
return StyleSheet.create(styles);
}, [theme]);
renderMasterKey(_num: number, mk: MasterKeyEntity) {
const theme = themeStyle(this.props.themeId);
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
};
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
const onPasswordChange = (text: string) => {
return shared.onPasswordChange(this, mk, text);
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
return (
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
<Text style={{ ...styles.normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
);
} else {
return (
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, props.passwords)}></Button>
</View>
);
}
@ -133,65 +114,65 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<Text style={styles.titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
{renderPasswordInput(mk.id)}
</View>
</View>
);
}
};
passwordPromptComponent() {
const theme = themeStyle(this.props.themeId);
const renderPasswordPrompt = () => {
const theme = themeStyle(props.themeId);
const masterKey = getDefaultMasterKey();
const onEnableClick = async () => {
try {
const password = this.state.passwordPromptAnswer;
const password = passwordPromptAnswer;
if (!password) throw new Error(_('Password cannot be empty'));
const password2 = this.state.passwordPromptConfirmAnswer;
const password2 = passwordPromptConfirmAnswer;
if (!password2) throw new Error(_('Confirm password cannot be empty'));
if (password !== password2) throw new Error(_('Passwords do not match!'));
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
this.setState({ passwordPromptShow: false });
setPasswordPromptShow(false);
} catch (error) {
await dialogs.error(this, error.message);
alert(error.message);
}
};
const messages = shared.enableEncryptionConfirmationMessages(masterKey);
const messages = enableEncryptionConfirmationMessages(masterKey);
const messageComps = messages.map(msg => {
const messageComps = messages.map((msg: string) => {
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
});
return (
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
<View>{messageComps}</View>
<Text style={this.styles().normalText}>{_('Password:')}</Text>
<Text style={styles.normalText}>{_('Password:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
style={this.styles().normalTextInput}
style={styles.normalTextInput}
secureTextEntry={true}
value={this.state.passwordPromptAnswer}
value={passwordPromptAnswer}
onChangeText={(text: string) => {
this.setState({ passwordPromptAnswer: text });
setPasswordPromptAnswer(text);
}}
></TextInput>
<Text style={this.styles().normalText}>{_('Confirm password:')}</Text>
<Text style={styles.normalText}>{_('Confirm password:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
style={this.styles().normalTextInput}
style={styles.normalTextInput}
secureTextEntry={true}
value={this.state.passwordPromptConfirmAnswer}
value={passwordPromptConfirmAnswer}
onChangeText={(text: string) => {
this.setState({ passwordPromptConfirmAnswer: text });
setPasswordPromptConfirmAnswer(text);
}}
></TextInput>
<View style={{ flexDirection: 'row' }}>
@ -207,156 +188,132 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
<Button
title={_('Cancel')}
onPress={() => {
this.setState({ passwordPromptShow: false });
setPasswordPromptShow(false);
}}
></Button>
</View>
</View>
</View>
);
}
};
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
const renderMasterPassword = () => {
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
if (this.state.passwordChecks['master']) {
if (passwordChecks['master']) {
return (
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
</View>
);
} else {
return (
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput>
<Button onPress={onMasterPasswordSave} title={_('Save')} />
</View>
</View>
);
}
};
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
mkComps.push(renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys;
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const onToggleButtonClick = async () => {
if (props.encryptionEnabled) {
const ok = await dialogs.confirmRef(dialogBoxRef.current, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
if (!ok) return;
const mkComps = [];
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
try {
await setupAndDisableEncryption(EncryptionService.instance());
} catch (error) {
alert(error.message);
}
} else {
setPasswordPromptShow(true);
setPasswordPromptAnswer('');
setPasswordPromptConfirmAnswer('');
return;
}
};
const onToggleButtonClick = async () => {
if (this.props.encryptionEnabled) {
const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
if (!ok) return;
let nonExistingMasterKeySection = null;
try {
await setupAndDisableEncryption(EncryptionService.instance());
} catch (error) {
await dialogs.error(this, error.message);
}
} else {
this.setState({
passwordPromptShow: true,
passwordPromptAnswer: '',
passwordPromptConfirmAnswer: '',
});
return;
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<Text style={this.styles().normalText} key={id}>
{id}
</Text>
);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<Text style={styles.normalText} key={id}>
{id}
</Text>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={this.styles().container}>
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{this.renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox
ref={(dialogbox: any) => {
this.dialogbox = dialogbox;
}}
/>
nonExistingMasterKeySection = (
<View>
<Text style={styles.titleText}>{_('Missing Master Keys')}</Text>
<Text style={styles.normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
);
}
}
const EncryptionConfigScreen = connect((state: State) => {
const passwordPromptComp = passwordPromptShow ? renderPasswordPrompt() : null;
const toggleButton = !passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={rootStyle}>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={styles.container}>
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={styles.titleText}>{_('Status')}</Text>
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox ref={dialogBoxRef}/>
</View>
);
};
export default connect((state: State) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
@ -368,6 +325,4 @@ const EncryptionConfigScreen = connect((state: State) => {
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
})(EncryptionConfigScreenComponent);
export default EncryptionConfigScreen;
})(EncryptionConfigScreen);

View File

@ -15,8 +15,8 @@
"postinstall": "jetify && npm run build"
},
"dependencies": {
"@joplin/lib": "^2.2.0",
"@joplin/renderer": "^2.2.0",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@react-native-community/clipboard": "^1.5.0",
"@react-native-community/datetimepicker": "^3.0.3",
"@react-native-community/geolocation": "^2.0.2",
@ -69,7 +69,7 @@
"@codemirror/lang-markdown": "^0.18.4",
"@codemirror/state": "^0.18.7",
"@codemirror/view": "^0.18.19",
"@joplin/tools": "^1.0.9",
"@joplin/tools": "~2.4",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/node": "^14.14.6",

View File

@ -1 +1 @@
module.exports = `LyoKCkF0b20gT25lIERhcmsgV2l0aCBzdXBwb3J0IGZvciBSZWFzb25NTCBieSBHaWRpIE1vcnJpcywgYmFzZWQgb2ZmIHdvcmsgYnkgRGFuaWVsIEdhbWFnZQoKT3JpZ2luYWwgT25lIERhcmsgU3ludGF4IHRoZW1lIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2F0b20vb25lLWRhcmstc3ludGF4CgoqLwouaGxqcyB7CiAgZGlzcGxheTogYmxvY2s7CiAgb3ZlcmZsb3cteDogYXV0bzsKICBwYWRkaW5nOiAwLjVlbTsKICBjb2xvcjogI2FiYjJiZjsKICBiYWNrZ3JvdW5kOiAjMjgyYzM0Owp9Ci5obGpzLWtleXdvcmQsIC5obGpzLW9wZXJhdG9yIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIC5obGpzLWNvbnN0cnVjdG9yIHsKICBjb2xvcjogIzYxYWVlZTsKfQouaGxqcy1mdW5jdGlvbiB7CiAgY29sb3I6ICM2MWFlZWU7Cn0KLmhsanMtZnVuY3Rpb24gLmhsanMtcGFyYW1zIHsKICBjb2xvcjogI0E2RTIyRTsKfQouaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5nIHsKICBjb2xvcjogI0ZEOTcxRjsKfQouaGxqcy1tb2R1bGUtYWNjZXNzIC5obGpzLW1vZHVsZSB7CiAgY29sb3I6ICM3ZTU3YzI7Cn0KLmhsanMtY29uc3RydWN0b3IgewogIGNvbG9yOiAjZTJiOTNkOwp9Ci5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZyB7CiAgY29sb3I6ICM5Q0NDNjU7Cn0KLmhsanMtY29tbWVudCwgLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYjE4ZWIxOwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQouaGxqcy1kb2N0YWcsIC5obGpzLWZvcm11bGEgewogIGNvbG9yOiAjYzY3OGRkOwp9Ci5obGpzLXNlY3Rpb24sIC5obGpzLW5hbWUsIC5obGpzLXNlbGVjdG9yLXRhZywgLmhsanMtZGVsZXRpb24sIC5obGpzLXN1YnN0IHsKICBjb2xvcjogI2UwNmM3NTsKfQouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzU2YjZjMjsKfQouaGxqcy1zdHJpbmcsIC5obGpzLXJlZ2V4cCwgLmhsanMtYWRkaXRpb24sIC5obGpzLWF0dHJpYnV0ZSwgLmhsanMtbWV0YS1zdHJpbmcgewogIGNvbG9yOiAjOThjMzc5Owp9Ci5obGpzLWJ1aWx0X2luLCAuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSB7CiAgY29sb3I6ICNlNmMwN2I7Cn0KLmhsanMtYXR0ciwgLmhsanMtdmFyaWFibGUsIC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLCAuaGxqcy10eXBlLCAuaGxqcy1zZWxlY3Rvci1jbGFzcywgLmhsanMtc2VsZWN0b3ItYXR0ciwgLmhsanMtc2VsZWN0b3ItcHNldWRvLCAuaGxqcy1udW1iZXIgewogIGNvbG9yOiAjZDE5YTY2Owp9Ci5obGpzLXN5bWJvbCwgLmhsanMtYnVsbGV0LCAuaGxqcy1saW5rLCAuaGxqcy1tZXRhLCAuaGxqcy1zZWxlY3Rvci1pZCwgLmhsanMtdGl0bGUgewogIGNvbG9yOiAjNjFhZWVlOwp9Ci5obGpzLWVtcGhhc2lzIHsKICBmb250LXN0eWxlOiBpdGFsaWM7Cn0KLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQouaGxqcy1saW5rIHsKICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsKfQo=`;
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiNhYmIyYmY7YmFja2dyb3VuZDojMjgyYzM0fS5obGpzLWtleXdvcmQsLmhsanMtb3BlcmF0b3IsLmhsanMtcGF0dGVybi1tYXRjaHtjb2xvcjojZjkyNjcyfS5obGpzLWZ1bmN0aW9uLC5obGpzLXBhdHRlcm4tbWF0Y2ggLmhsanMtY29uc3RydWN0b3J7Y29sb3I6IzYxYWVlZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXN7Y29sb3I6I2E2ZTIyZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5ne2NvbG9yOiNmZDk3MWZ9LmhsanMtbW9kdWxlLWFjY2VzcyAuaGxqcy1tb2R1bGV7Y29sb3I6IzdlNTdjMn0uaGxqcy1jb25zdHJ1Y3Rvcntjb2xvcjojZTJiOTNkfS5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZ3tjb2xvcjojOWNjYzY1fS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2IxOGViMTtmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYXtjb2xvcjojYzY3OGRkfS5obGpzLWRlbGV0aW9uLC5obGpzLW5hbWUsLmhsanMtc2VjdGlvbiwuaGxqcy1zZWxlY3Rvci10YWcsLmhsanMtc3Vic3R7Y29sb3I6I2UwNmM3NX0uaGxqcy1saXRlcmFse2NvbG9yOiM1NmI2YzJ9LmhsanMtYWRkaXRpb24sLmhsanMtYXR0cmlidXRlLC5obGpzLW1ldGEgLmhsanMtc3RyaW5nLC5obGpzLXJlZ2V4cCwuaGxqcy1zdHJpbmd7Y29sb3I6Izk4YzM3OX0uaGxqcy1idWlsdF9pbiwuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSwuaGxqcy10aXRsZS5jbGFzc197Y29sb3I6I2U2YzA3Yn0uaGxqcy1hdHRyLC5obGpzLW51bWJlciwuaGxqcy1zZWxlY3Rvci1hdHRyLC5obGpzLXNlbGVjdG9yLWNsYXNzLC5obGpzLXNlbGVjdG9yLXBzZXVkbywuaGxqcy10ZW1wbGF0ZS12YXJpYWJsZSwuaGxqcy10eXBlLC5obGpzLXZhcmlhYmxle2NvbG9yOiNkMTlhNjZ9LmhsanMtYnVsbGV0LC5obGpzLWxpbmssLmhsanMtbWV0YSwuaGxqcy1zZWxlY3Rvci1pZCwuaGxqcy1zeW1ib2wsLmhsanMtdGl0bGV7Y29sb3I6IzYxYWVlZX0uaGxqcy1lbXBoYXNpc3tmb250LXN0eWxlOml0YWxpY30uaGxqcy1zdHJvbmd7Zm9udC13ZWlnaHQ6NzAwfS5obGpzLWxpbmt7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZX0=`;

View File

@ -1 +1 @@
module.exports = `LyoKCkF0b20gT25lIExpZ2h0IGJ5IERhbmllbCBHYW1hZ2UKT3JpZ2luYWwgT25lIExpZ2h0IFN5bnRheCB0aGVtZSBmcm9tIGh0dHBzOi8vZ2l0aHViLmNvbS9hdG9tL29uZS1saWdodC1zeW50YXgKCmJhc2U6ICAgICNmYWZhZmEKbW9uby0xOiAgIzM4M2E0Mgptb25vLTI6ICAjNjg2Yjc3Cm1vbm8tMzogICNhMGExYTcKaHVlLTE6ICAgIzAxODRiYgpodWUtMjogICAjNDA3OGYyCmh1ZS0zOiAgICNhNjI2YTQKaHVlLTQ6ICAgIzUwYTE0ZgpodWUtNTogICAjZTQ1NjQ5Cmh1ZS01LTI6ICNjOTEyNDMKaHVlLTY6ICAgIzk4NjgwMQpodWUtNi0yOiAjYzE4NDAxCgoqLwoKLmhsanMgewogIGRpc3BsYXk6IGJsb2NrOwogIG92ZXJmbG93LXg6IGF1dG87CiAgcGFkZGluZzogMC41ZW07CiAgY29sb3I6ICMzODNhNDI7CiAgYmFja2dyb3VuZDogI2ZhZmFmYTsKfQoKLmhsanMtY29tbWVudCwKLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYTBhMWE3OwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtZG9jdGFnLAouaGxqcy1rZXl3b3JkLAouaGxqcy1mb3JtdWxhIHsKICBjb2xvcjogI2E2MjZhNDsKfQoKLmhsanMtc2VjdGlvbiwKLmhsanMtbmFtZSwKLmhsanMtc2VsZWN0b3ItdGFnLAouaGxqcy1kZWxldGlvbiwKLmhsanMtc3Vic3QgewogIGNvbG9yOiAjZTQ1NjQ5Owp9CgouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzAxODRiYjsKfQoKLmhsanMtc3RyaW5nLAouaGxqcy1yZWdleHAsCi5obGpzLWFkZGl0aW9uLAouaGxqcy1hdHRyaWJ1dGUsCi5obGpzLW1ldGEtc3RyaW5nIHsKICBjb2xvcjogIzUwYTE0ZjsKfQoKLmhsanMtYnVpbHRfaW4sCi5obGpzLWNsYXNzIC5obGpzLXRpdGxlIHsKICBjb2xvcjogI2MxODQwMTsKfQoKLmhsanMtYXR0ciwKLmhsanMtdmFyaWFibGUsCi5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLAouaGxqcy10eXBlLAouaGxqcy1zZWxlY3Rvci1jbGFzcywKLmhsanMtc2VsZWN0b3ItYXR0ciwKLmhsanMtc2VsZWN0b3ItcHNldWRvLAouaGxqcy1udW1iZXIgewogIGNvbG9yOiAjOTg2ODAxOwp9CgouaGxqcy1zeW1ib2wsCi5obGpzLWJ1bGxldCwKLmhsanMtbGluaywKLmhsanMtbWV0YSwKLmhsanMtc2VsZWN0b3ItaWQsCi5obGpzLXRpdGxlIHsKICBjb2xvcjogIzQwNzhmMjsKfQoKLmhsanMtZW1waGFzaXMgewogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQoKLmhsanMtbGluayB7CiAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7Cn0K`;
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiMzODNhNDI7YmFja2dyb3VuZDojZmFmYWZhfS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2EwYTFhNztmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYSwuaGxqcy1rZXl3b3Jke2NvbG9yOiNhNjI2YTR9LmhsanMtZGVsZXRpb24sLmhsanMtbmFtZSwuaGxqcy1zZWN0aW9uLC5obGpzLXNlbGVjdG9yLXRhZywuaGxqcy1zdWJzdHtjb2xvcjojZTQ1NjQ5fS5obGpzLWxpdGVyYWx7Y29sb3I6IzAxODRiYn0uaGxqcy1hZGRpdGlvbiwuaGxqcy1hdHRyaWJ1dGUsLmhsanMtbWV0YSAuaGxqcy1zdHJpbmcsLmhsanMtcmVnZXhwLC5obGpzLXN0cmluZ3tjb2xvcjojNTBhMTRmfS5obGpzLWF0dHIsLmhsanMtbnVtYmVyLC5obGpzLXNlbGVjdG9yLWF0dHIsLmhsanMtc2VsZWN0b3ItY2xhc3MsLmhsanMtc2VsZWN0b3ItcHNldWRvLC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLC5obGpzLXR5cGUsLmhsanMtdmFyaWFibGV7Y29sb3I6Izk4NjgwMX0uaGxqcy1idWxsZXQsLmhsanMtbGluaywuaGxqcy1tZXRhLC5obGpzLXNlbGVjdG9yLWlkLC5obGpzLXN5bWJvbCwuaGxqcy10aXRsZXtjb2xvcjojNDA3OGYyfS5obGpzLWJ1aWx0X2luLC5obGpzLWNsYXNzIC5obGpzLXRpdGxlLC5obGpzLXRpdGxlLmNsYXNzX3tjb2xvcjojYzE4NDAxfS5obGpzLWVtcGhhc2lze2ZvbnQtc3R5bGU6aXRhbGljfS5obGpzLXN0cm9uZ3tmb250LXdlaWdodDo3MDB9LmhsanMtbGlua3t0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lfQ==`;

View File

@ -1,5 +1,5 @@
module.exports = {
hash:"6608023b8053b48e0eec248644475e33", files: {
hash:"de3871f000c87478973d7cd0913bd3ff", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

File diff suppressed because one or more lines are too long

View File

@ -7,14 +7,13 @@ const { Keyboard } = require('react-native');
const dialogs = {};
dialogs.confirm = (parentComponent, message) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
dialogs.confirmRef = (ref, message) => {
if (!ref) throw new Error('ref is required');
return new Promise((resolve) => {
Keyboard.dismiss();
parentComponent.dialogbox.confirm({
ref.confirm({
content: message,
ok: {
@ -32,6 +31,13 @@ dialogs.confirm = (parentComponent, message) => {
});
};
dialogs.confirm = (parentComponent, message) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
return dialogs.confirmRef(parentComponent.dialogBox, message);
};
dialogs.pop = (parentComponent, message, buttons, options = null) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');

View File

@ -0,0 +1,183 @@
import shim from '../../shim';
import { _ } from '../../locale';
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
import EncryptionService from '../../services/e2ee/EncryptionService';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry';
import Setting from '../../models/Setting';
const { useCallback, useEffect, useState } = shim.react();
type PasswordChecks = Record<string, boolean>;
export const useStats = () => {
const [stats, setStats] = useState<EncryptedItemsStats>({ encrypted: null, total: null });
const [statsUpdateTime, setStatsUpdateTime] = useState<number>(0);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const r = await BaseItem.encryptedItemsStats();
if (event.cancelled) return;
setStats(r);
}, [statsUpdateTime]);
useEffect(() => {
const iid = shim.setInterval(() => {
setStatsUpdateTime(Date.now());
}, 3000);
return () => {
clearInterval(iid);
};
}, []);
return stats;
};
export const decryptedStatText = (stats: EncryptedItemsStats) => {
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
return result;
};
export const enableEncryptionConfirmationMessages = (masterKey: MasterKeyEntity) => {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
return msg;
};
const masterPasswordIsValid = async (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string = null) => {
const activeMasterKey = masterKeys.find((mk: MasterKeyEntity) => mk.id === activeMasterKeyId);
masterPassword = masterPassword === null ? masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
};
export const reencryptData = async () => {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
await BaseItem.forceSyncAll();
void reg.waitForSyncFinishedThenSync();
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
alert(_('Your data is going to be re-encrypted and synced again.'));
};
export const dontReencryptData = () => {
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
};
export const useToggleShowDisabledMasterKeys = () => {
const [showDisabledMasterKeys, setShowDisabledMasterKeys] = useState<boolean>(false);
const toggleShowDisabledMasterKeys = () => {
setShowDisabledMasterKeys((current) => !current);
};
return { showDisabledMasterKeys, toggleShowDisabledMasterKeys };
};
export const onToggleEnabledClick = (mk: MasterKeyEntity) => {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
};
export const onSavePasswordClick = (mk: MasterKeyEntity, passwords: Record<string, string>) => {
const password = passwords[mk.id];
if (!password) {
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
} else {
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
}
};
export const onMasterPasswordSave = (masterPasswordInput: string) => {
Setting.setValue('encryption.masterPassword', masterPasswordInput);
};
export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string) => {
const [inputMasterPassword, setInputMasterPassword] = useState<string>('');
const onMasterPasswordSave = useCallback(async () => {
Setting.setValue('encryption.masterPassword', inputMasterPassword);
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
alert('Password is invalid. Please try again.');
}
}, [inputMasterPassword]);
const onMasterPasswordChange = useCallback((password: string) => {
setInputMasterPassword(password);
}, []);
return { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange };
};
export const useInputPasswords = (propsPasswords: Record<string, string>) => {
const [inputPasswords, setInputPasswords] = useState<Record<string, string>>(propsPasswords);
useEffect(() => {
setInputPasswords(propsPasswords);
}, [propsPasswords]);
const onInputPasswordChange = useCallback((mk: MasterKeyEntity, password: string) => {
setInputPasswords(current => {
return {
...current,
[mk.id]: password,
};
});
}, []);
return { inputPasswords, onInputPasswordChange };
};
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
useAsyncEffect(async (event: AsyncEffectEvent) => {
const newPasswordChecks: PasswordChecks = {};
const newMasterPasswordKeys: PasswordChecks = {};
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwords);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
newPasswordChecks[mk.id] = ok;
newMasterPasswordKeys[mk.id] = password === masterPassword;
}
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
if (event.cancelled) return;
setPasswordChecks(newPasswordChecks);
setMasterPasswordKeys(newMasterPasswordKeys);
}, [masterKeys, masterPassword]);
return { passwordChecks, masterPasswordKeys };
};
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
const passwordCheck = passwordChecks[masterKey.id];
if (!passwordCheck) {
return _('Please enter your password in the master key list below before upgrading the key.');
}
try {
const password = passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
return _('The master key has been upgraded successfully!');
} catch (error) {
return _('Could not upgrade master key: %s', error.message);
}
};

View File

@ -1,196 +0,0 @@
import EncryptionService from '../../services/e2ee/EncryptionService';
import { _ } from '../../locale';
import BaseItem from '../../models/BaseItem';
import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry.js';
import shim from '../../shim';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
class Shared {
private refreshStatsIID_: any;
public initialize(comp: any, props: any) {
comp.state = {
passwordChecks: {},
// Master keys that can be decrypted with the master password
// (normally all of them, but for legacy support we need this).
masterPasswordKeys: {},
stats: {
encrypted: null,
total: null,
},
passwords: Object.assign({}, props.passwords),
showDisabledMasterKeys: false,
masterPasswordInput: '',
};
comp.isMounted_ = false;
this.refreshStatsIID_ = null;
}
public async refreshStats(comp: any) {
const stats = await BaseItem.encryptedItemsStats();
comp.setState({
stats: stats,
});
}
public async toggleShowDisabledMasterKeys(comp: any) {
comp.setState({ showDisabledMasterKeys: !comp.state.showDisabledMasterKeys });
}
public async reencryptData() {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
await BaseItem.forceSyncAll();
void reg.waitForSyncFinishedThenSync();
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
alert(_('Your data is going to be re-encrypted and synced again.'));
}
public dontReencryptData() {
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
}
public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) {
const passwordCheck = comp.state.passwordChecks[masterKey.id];
if (!passwordCheck) {
alert(_('Please enter your password in the master key list below before upgrading the key.'));
return;
}
try {
const password = comp.state.passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
alert(_('The master key has been upgraded successfully!'));
} catch (error) {
alert(_('Could not upgrade master key: %s', error.message));
}
}
public componentDidMount(comp: any) {
this.componentDidUpdate(comp);
void this.refreshStats(comp);
if (this.refreshStatsIID_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
}
this.refreshStatsIID_ = shim.setInterval(() => {
if (!comp.isMounted_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
return;
}
void this.refreshStats(comp);
}, 3000);
}
public componentDidUpdate(comp: any, prevProps: any = null) {
if (prevProps && comp.props.passwords !== prevProps.passwords) {
comp.setState({ passwords: Object.assign({}, comp.props.passwords) });
}
if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) {
comp.checkPasswords();
}
}
public componentWillUnmount() {
if (this.refreshStatsIID_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
}
}
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
}
public async checkPasswords(comp: any) {
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
for (let i = 0; i < comp.props.masterKeys.length; i++) {
const mk = comp.props.masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
passwordChecks[mk.id] = ok;
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
}
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
comp.setState({ passwordChecks, masterPasswordKeys });
}
public masterPasswordStatus(comp: any) {
// Don't translate for now because that's temporary - later it should
// always be set and the label should be replaced by a "Change master
// password" button.
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
}
public decryptedStatText(comp: any) {
const stats = comp.state.stats;
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
return result;
}
public onSavePasswordClick(comp: any, mk: MasterKeyEntity) {
const password = comp.state.passwords[mk.id];
if (!password) {
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
} else {
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
}
comp.checkPasswords();
}
public onMasterPasswordChange(comp: any, value: string) {
comp.setState({ masterPasswordInput: value });
}
public onMasterPasswordSave(comp: any) {
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
}
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
const passwords = Object.assign({}, comp.state.passwords);
passwords[mk.id] = password;
comp.setState({ passwords: passwords });
}
public onToggleEnabledClick(_comp: any, mk: MasterKeyEntity) {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
}
public enableEncryptionConfirmationMessages(masterKey: MasterKeyEntity) {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
return msg;
}
}
const shared = new Shared();
export default shared;

View File

@ -25,6 +25,7 @@ function useEventListener(
const eventListener = (event: Event) => {
// eslint-disable-next-line no-extra-boolean-cast
if (!!savedHandler?.current) {
// @ts-ignore
savedHandler.current(event);
}
};

View File

@ -33,6 +33,11 @@ export interface ItemsThatNeedSyncResult {
neverSyncedItemIds: string[];
}
export interface EncryptedItemsStats {
encrypted: number;
total: number;
}
export default class BaseItem extends BaseModel {
public static encryptionService_: any = null;
@ -513,7 +518,7 @@ export default class BaseItem extends BaseModel {
return output;
}
static async encryptedItemsStats() {
public static async encryptedItemsStats(): Promise<EncryptedItemsStats> {
const classNames = this.encryptableItemClassNames();
let encryptedCount = 0;
let totalCount = 0;

View File

@ -67,6 +67,7 @@
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/react": "^17.0.20",
"clean-html": "^1.5.0",
"jest": "^26.6.3",
"sharp": "^0.26.2",
@ -1073,6 +1074,29 @@
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"node_modules/@types/react": {
"version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"node_modules/@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@ -2253,6 +2277,12 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
"node_modules/csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -5639,6 +5669,7 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.",
"optional": true,
"dependencies": {
"block-stream": "*",
@ -9758,6 +9789,29 @@
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/react": {
"version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@ -10725,6 +10779,12 @@
}
}
},
"csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -15643,7 +15703,6 @@
},
"uslug": {
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
"requires": {
"node-emoji": "^1.10.0",

View File

@ -19,6 +19,7 @@
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/react": "^17.0.20",
"clean-html": "^1.5.0",
"jest": "^26.6.3",
"sharp": "^0.26.2",

View File

@ -589,4 +589,25 @@ describe('reducer', function() {
expect(state.selectedFolderId).toEqual(null);
expect(state.selectedNoteIds[0]).toEqual(notes[1].id);
});
// tests for NOTE_UPDATE_ALL about issue #5447
it('should not change selectedNoteIds object when selections are not changed', async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
{
// Case 1. Selected notes are changed when one of selected notes is deleted.
let state = initTestState(folders, 0, notes, [0, 2, 4]);
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes.slice(0, 4), notesSource: 'test' });
const expected = [notes[0].id, notes[2].id].sort();
expect([...state.selectedNoteIds].sort()).toEqual(expected);
}
{
// Case 2. Selected notes and object identity are unchanged when notes are not changed.
let state = initTestState(folders, 0, notes, [0, 2, 4]);
const expected = state.selectedNoteIds;
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: 'test' });
// Object identity is checked. Don't use toEqual() or toStrictEqual() here.
expect(state.selectedNoteIds).toBe(expected);
}
});
});

View File

@ -433,7 +433,7 @@ function updateSelectedNotesFromExistingNotes(draft: Draft<State>) {
}
}
}
if (JSON.stringify(draft.selectedNoteIds) === JSON.stringify(newSelectedNoteIds)) return;
draft.selectedNoteIds = newSelectedNoteIds;
}

View File

@ -103,7 +103,7 @@ export async function migrateMasterPassword() {
// previously any master key could be encrypted with any password, so to support
// this legacy case, we first check if the MK decrypts with the master password.
// If not, try with the master key specific password, if any is defined.
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity, passwordCache: Record<string, string> = null): Promise<string> {
const masterPassword = Setting.value('encryption.masterPassword');
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
logger.info('findMasterKeyPassword: Using master password');
@ -112,7 +112,7 @@ export async function findMasterKeyPassword(service: EncryptionService, masterKe
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
const passwords = Setting.value('encryption.passwordCache');
const passwords = passwordCache ? passwordCache : Setting.value('encryption.passwordCache');
return passwords[masterKey.id];
}

View File

@ -1,22 +1,26 @@
import * as React from 'react';
import { NoteEntity, ResourceEntity } from './services/database/types';
let isTestingEnv_ = false;
// We need to ensure that there's only one instance of React being used by
// all the packages. In particular, the lib might need React to define
// generic hooks, but it shouldn't have React in its dependencies as that
// would cause the following error:
// We need to ensure that there's only one instance of React being used by all
// the packages. In particular, the lib might need React to define generic
// hooks, but it shouldn't have React in its dependencies as that would cause
// the following error:
//
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
//
// So instead, the **applications** include React as a dependency, then
// pass it to any other packages using the shim. Essentially, only one
// package should require React, and in our case that should be one of the
// applications (app-desktop, app-mobile, etc.) since we are sure they
// won't be dependency to other packages (unlike the lib which can be
// included anywhere).
let react_: any = null;
// So instead, the **applications** include React as a dependency, then pass it
// to any other packages using the shim. Essentially, only one package should
// require React, and in our case that should be one of the applications
// (app-desktop, app-mobile, etc.) since we are sure they won't be dependency to
// other packages (unlike the lib which can be included anywhere).
//
// Regarding the type - althought we import React, we only use it as a type
// using `typeof React`. This is just to get types in hooks.
//
// https://stackoverflow.com/a/42816077/561309
let react_: typeof React = null;
const shim = {
Geolocation: null as any,

View File

@ -1,75 +1 @@
/*
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #abb2bf;
background: #282c34;
}
.hljs-keyword, .hljs-operator {
color: #F92672;
}
.hljs-pattern-match {
color: #F92672;
}
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #A6E22E;
}
.hljs-function .hljs-params .hljs-typing {
color: #FD971F;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9CCC65;
}
.hljs-comment, .hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag, .hljs-formula {
color: #c678dd;
}
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
color: #98c379;
}
.hljs-built_in, .hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
color: #d19a66;
}
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-keyword,.hljs-operator,.hljs-pattern-match{color:#f92672}.hljs-function,.hljs-pattern-match .hljs-constructor{color:#61aeee}.hljs-function .hljs-params{color:#a6e22e}.hljs-function .hljs-params .hljs-typing{color:#fd971f}.hljs-module-access .hljs-module{color:#7e57c2}.hljs-constructor{color:#e2b93d}.hljs-constructor .hljs-string{color:#9ccc65}.hljs-comment,.hljs-quote{color:#b18eb1;font-style:italic}.hljs-doctag,.hljs-formula{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

View File

@ -1,96 +1 @@
/*
Atom One Light by Daniel Gamage
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
base: #fafafa
mono-1: #383a42
mono-2: #686b77
mono-3: #a0a1a7
hue-1: #0184bb
hue-2: #4078f2
hue-3: #a626a4
hue-4: #50a14f
hue-5: #e45649
hue-5-2: #c91243
hue-6: #986801
hue-6-2: #c18401
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #a626a4;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #50a14f;
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #c18401;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #986801;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #4078f2;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@
"markdown-it-sup": "^1.0.0",
"markdown-it-toc-done-right": "^4.1.0",
"md5": "^2.2.1",
"mermaid": "^8.10.2",
"mermaid": "^8.12.1",
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
},
"devDependencies": {
@ -1998,7 +1998,8 @@
"node_modules/buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"node_modules/cache-base": {
"version": "1.0.1",
@ -2029,15 +2030,6 @@
"node": ">=6"
}
},
"node_modules/camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"dependencies": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@ -2135,25 +2127,6 @@
"node": ">=0.10.0"
}
},
"node_modules/clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
"integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/clean-css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -2286,17 +2259,6 @@
"node": "*"
}
},
"node_modules/css-b64-images": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=",
"bin": {
"css-b64-images": "bin/css-b64-images"
},
"engines": {
"node": "*"
}
},
"node_modules/cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -2647,6 +2609,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
@ -2797,6 +2760,11 @@
"node": ">=8"
}
},
"node_modules/dompurify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -2839,14 +2807,6 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"node_modules/entity-decode": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz",
"integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==",
"dependencies": {
"he": "^1.1.1"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -3542,14 +3502,6 @@
"node": ">=0.10.0"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
@ -3590,26 +3542,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/html-minifier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
"dependencies": {
"camel-case": "^3.0.0",
"clean-css": "^4.2.1",
"commander": "^2.19.0",
"he": "^1.2.0",
"param-case": "^2.1.1",
"relateurl": "^0.2.7",
"uglify-js": "^3.5.1"
},
"bin": {
"html-minifier": "cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@ -6045,11 +5977,6 @@
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"node_modules/lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -6234,21 +6161,19 @@
"dev": true
},
"node_modules/mermaid": {
"version": "8.10.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.10.2.tgz",
"integrity": "sha512-Za5MrbAOMbEsyY4ONgGjfYz06sbwF1iNGRzp1sQqpOtvXxjxGu/J1jRJ8QyE9kD/D9zj1/KlRrYegWEvA7eZ5Q==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.12.1.tgz",
"integrity": "sha512-0UCcSF0FLoNcPBsRF4f9OIV32t41fV18//z8o3S+FDz2PbDA1CRGKdQF9IX84VP4Tv9kcgJI/oqJdcBEtB/GPA==",
"dependencies": {
"@braintree/sanitize-url": "^3.1.0",
"d3": "^5.7.0",
"dagre": "^0.8.4",
"d3": "^5.16.0",
"dagre": "^0.8.5",
"dagre-d3": "^0.6.4",
"entity-decode": "^2.0.2",
"graphlib": "^2.1.7",
"he": "^1.2.0",
"khroma": "^1.1.0",
"minify": "^4.1.1",
"moment-mini": "^2.22.1",
"stylis": "^3.5.2"
"dompurify": "2.3.1",
"graphlib": "^2.1.8",
"khroma": "^1.4.1",
"moment-mini": "^2.24.0",
"stylis": "^4.0.10"
}
},
"node_modules/micromatch": {
@ -6294,26 +6219,6 @@
"node": ">=6"
}
},
"node_modules/minify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
"dependencies": {
"clean-css": "^4.1.6",
"css-b64-images": "~0.2.5",
"debug": "^4.1.0",
"html-minifier": "^4.0.0",
"terser": "^4.0.0",
"try-catch": "^2.0.0",
"try-to-catch": "^1.0.2"
},
"bin": {
"minify": "bin/minify.js"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -6365,7 +6270,8 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/nanomatch": {
"version": "1.2.13",
@ -6401,14 +6307,6 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"dependencies": {
"lower-case": "^1.1.1"
}
},
"node_modules/node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@ -6684,14 +6582,6 @@
"node": ">=6"
}
},
"node_modules/param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"dependencies": {
"no-case": "^2.2.0"
}
},
"node_modules/parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
@ -6973,14 +6863,6 @@
"node": ">=0.10.0"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -7661,6 +7543,7 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -7670,6 +7553,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -7876,9 +7760,9 @@
}
},
"node_modules/stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
},
"node_modules/supports-color": {
"version": "5.5.0",
@ -7948,30 +7832,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/terser/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -8084,19 +7944,6 @@
"node": ">=8"
}
},
"node_modules/try-catch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/try-to-catch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -8172,17 +8019,6 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/uglify-js": {
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
"integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==",
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -8262,11 +8098,6 @@
"node": ">=0.10.0"
}
},
"node_modules/upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
},
"node_modules/uri-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
@ -10235,7 +10066,8 @@
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"cache-base": {
"version": "1.0.1",
@ -10260,15 +10092,6 @@
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
"camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"requires": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
}
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@ -10347,21 +10170,6 @@
}
}
},
"clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
"integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
"requires": {
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -10475,11 +10283,6 @@
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"css-b64-images": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI="
},
"cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -10811,6 +10614,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
@ -10921,6 +10725,11 @@
}
}
},
"dompurify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -10957,14 +10766,6 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"entity-decode": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz",
"integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==",
"requires": {
"he": "^1.1.1"
}
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -11512,11 +11313,6 @@
}
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"highlight.js": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
@ -11548,20 +11344,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"html-minifier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
"requires": {
"camel-case": "^3.0.0",
"clean-css": "^4.2.1",
"commander": "^2.19.0",
"he": "^1.2.0",
"param-case": "^2.1.1",
"relateurl": "^0.2.7",
"uglify-js": "^3.5.1"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@ -13427,11 +13209,6 @@
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -13594,21 +13371,19 @@
"dev": true
},
"mermaid": {
"version": "8.10.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.10.2.tgz",
"integrity": "sha512-Za5MrbAOMbEsyY4ONgGjfYz06sbwF1iNGRzp1sQqpOtvXxjxGu/J1jRJ8QyE9kD/D9zj1/KlRrYegWEvA7eZ5Q==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.12.1.tgz",
"integrity": "sha512-0UCcSF0FLoNcPBsRF4f9OIV32t41fV18//z8o3S+FDz2PbDA1CRGKdQF9IX84VP4Tv9kcgJI/oqJdcBEtB/GPA==",
"requires": {
"@braintree/sanitize-url": "^3.1.0",
"d3": "^5.7.0",
"dagre": "^0.8.4",
"d3": "^5.16.0",
"dagre": "^0.8.5",
"dagre-d3": "^0.6.4",
"entity-decode": "^2.0.2",
"graphlib": "^2.1.7",
"he": "^1.2.0",
"khroma": "^1.1.0",
"minify": "^4.1.1",
"moment-mini": "^2.22.1",
"stylis": "^3.5.2"
"dompurify": "2.3.1",
"graphlib": "^2.1.8",
"khroma": "^1.4.1",
"moment-mini": "^2.24.0",
"stylis": "^4.0.10"
}
},
"micromatch": {
@ -13642,20 +13417,6 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"minify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
"requires": {
"clean-css": "^4.1.6",
"css-b64-images": "~0.2.5",
"debug": "^4.1.0",
"html-minifier": "^4.0.0",
"terser": "^4.0.0",
"try-catch": "^2.0.0",
"try-to-catch": "^1.0.2"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -13700,7 +13461,8 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"nanomatch": {
"version": "1.2.13",
@ -13733,14 +13495,6 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"requires": {
"lower-case": "^1.1.1"
}
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@ -13951,14 +13705,6 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"requires": {
"no-case": "^2.2.0"
}
},
"parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
@ -14172,11 +13918,6 @@
"safe-regex": "^1.1.0"
}
},
"relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -14731,6 +14472,7 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -14739,7 +14481,8 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@ -14905,9 +14648,9 @@
"dev": true
},
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
},
"supports-color": {
"version": "5.5.0",
@ -14961,23 +14704,6 @@
"supports-hyperlinks": "^2.0.0"
}
},
"terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -15068,16 +14794,6 @@
"punycode": "^2.1.1"
}
},
"try-catch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg=="
},
"try-to-catch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -15134,11 +14850,6 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-js": {
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
"integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g=="
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -15201,11 +14912,6 @@
}
}
},
"upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
},
"uri-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
@ -15229,7 +14935,6 @@
},
"uslug": {
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
"requires": {
"node-emoji": "^1.10.0",

View File

@ -45,7 +45,7 @@
"markdown-it-sup": "^1.0.0",
"markdown-it-toc-done-right": "^4.1.0",
"md5": "^2.2.1",
"mermaid": "^8.10.2",
"mermaid": "^8.12.1",
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
},
"gitHead": "80c0089d2c52aff608b2bea74389de5a7f12f2e2"

View File

@ -7,6 +7,9 @@ import { Models } from './factory';
import * as EventEmitter from 'events';
import { Config } from '../utils/types';
import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/personalizedUserContentBaseUrl';
import Logger from '@joplin/lib/Logger';
const logger = Logger.create('BaseModel');
export interface SaveOptions {
isNew?: boolean;
@ -163,33 +166,39 @@ export default abstract class BaseModel<T> {
//
// The `name` argument is only for debugging, so that any stuck transaction
// can be more easily identified.
protected async withTransaction<T>(fn: Function, name: string = null): Promise<T> {
const debugTransaction = false;
protected async withTransaction<T>(fn: Function, name: string): Promise<T> {
const debugSteps = false;
const debugTimeout = true;
const timeoutMs = 10000;
const debugTimerId = debugTransaction ? setTimeout(() => {
console.info('Transaction did not complete:', name, txIndex);
}, 5000) : null;
let txIndex = 0;
const txIndex = await this.transactionHandler_.start();
const debugTimerId = debugTimeout ? setTimeout(() => {
logger.error('Transaction did not complete:', name, txIndex);
logger.error('Transaction stack:');
logger.error(this.transactionHandler_.stackInfo);
}, timeoutMs) : null;
if (debugTransaction) console.info('START', name, txIndex);
txIndex = await this.transactionHandler_.start(name);
if (debugSteps) console.info('START', name, txIndex);
let output: T = null;
try {
output = await fn();
} catch (error) {
await this.transactionHandler_.rollback(txIndex);
if (debugTransaction) {
if (debugSteps) {
console.info('ROLLBACK', name, txIndex);
clearTimeout(debugTimerId);
}
await this.transactionHandler_.rollback(txIndex);
throw error;
}
if (debugTransaction) {
if (debugSteps) {
console.info('COMMIT', name, txIndex);
clearTimeout(debugTimerId);
}

View File

@ -34,7 +34,7 @@ export default class ItemResourceModel extends BaseModel<ItemResource> {
resource_id: resourceId,
});
}
});
}, 'ItemResourceModel::addResourceIds');
}
public async byItemId(itemId: Uuid): Promise<string[]> {

View File

@ -48,7 +48,7 @@ export default class NotificationModel extends BaseModel<KeyValue> {
value: this.serializeValue(value),
type,
});
});
}, 'KeyValueModel::setValue');
}
public async value<T>(key: string, defaultValue: Value = null): Promise<T> {

View File

@ -315,7 +315,7 @@ export default class ShareModel extends BaseModel<Share> {
for (const item of items) {
await this.models().userItem().add(userId, item.id);
}
});
}, 'ShareModel::createSharedFolderUserItems');
}
public async shareFolder(owner: User, folderId: string): Promise<Share> {

View File

@ -126,7 +126,7 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
}
return this.save({ ...shareUser, status });
});
}, 'ShareUserModel::setStatus');
}
public async deleteByShare(share: Share): Promise<void> {

View File

@ -91,7 +91,7 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
last_payment_time: now,
last_payment_failed_time: 0,
});
});
}, 'SubscriptionModel::handlePayment');
} else {
// We only update the payment failed time if it's not already set
// since the only thing that matter is the first time the payment
@ -145,7 +145,7 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
});
return { user, subscription };
});
}, 'SubscriptionModel::saveUserAndSubscription');
}
public async toggleSoftDelete(id: number, isDeleted: boolean) {

View File

@ -70,7 +70,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
await this.add(userId, flagType, { updateUser: false });
}
await this.updateUserFromFlags(userId);
});
}, 'UserFlagModels::addMulti');
}
public async removeMulti(userId: Uuid, flagTypes: UserFlagType[]) {
@ -79,7 +79,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
await this.remove(userId, flagType, { updateUser: false });
}
await this.updateUserFromFlags(userId);
});
}, 'UserFlagModels::removeMulti');
}
// As a general rule the `enabled` and `can_upload` properties should not

View File

@ -141,7 +141,7 @@ export default class UserItemModel extends BaseModel<UserItem> {
}
return super.save(userItem, options);
});
}, 'UserItemModel::save');
}
public async delete(_id: string | string[], _options: DeleteOptions = {}): Promise<void> {

View File

@ -369,7 +369,7 @@ export default class UserModel extends BaseModel<User> {
key: `payment_failed_upload_disabled_${sub.last_payment_failed_time}`,
});
}
});
}, 'UserModel::handleFailedPaymentSubscriptions');
}
public async handleOversizedAccounts() {
@ -430,7 +430,7 @@ export default class UserModel extends BaseModel<User> {
});
}
}
});
}, 'UserModel::handleOversizedAccounts');
}
private formatValues(user: User): User {
@ -466,7 +466,7 @@ export default class UserModel extends BaseModel<User> {
if (isNew) UserModel.eventEmitter.emit('created');
return savedUser;
});
}, 'UserModel::save');
}
}

View File

@ -137,6 +137,14 @@ export const postHandlers: PostHandlers = {
//
// - The public config is under packages/server/stripeConfig.json
// - The private config is in the server .env file
//
// # Failed Stripe cli login
//
// If the tool show this error, with code "api_key_expired":
//
// > FATAL Error while authenticating with Stripe: Authorization failed
//
// Need to logout and login again to refresh the CLI token - `stripe logout && stripe login`
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext, event: Stripe.Event = null, logErrors: boolean = true) => {
event = event ? event : await stripeEvent(stripe, ctx.req);
@ -426,7 +434,7 @@ const getHandlers: Record<string, StripeRouteHandler> = {
<body>
<button id="checkout">Subscribe</button>
<script>
var PRICE_ID = ${basicPrice.id};
var PRICE_ID = ${JSON.stringify(basicPrice.id)};
function handleResult() {
console.info('Redirected to checkout');

View File

@ -15,7 +15,7 @@ import uuidgen from '../../utils/uuidgen';
import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings';
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
import { yesNoDefaultOptions, yesNoOptions } from '../../utils/views/select';
import { confirmUrl } from '../../utils/urlUtils';
import { confirmUrl, stripePortalUrl } from '../../utils/urlUtils';
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
import { createCsrfTag } from '../../utils/csrf';
import { formatDateTime } from '../../utils/time';
@ -175,6 +175,7 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
view.content.canShareFolderOptions = yesNoDefaultOptions(user, 'can_share_folder');
view.content.canUploadOptions = yesNoOptions(user, 'can_upload');
view.content.userFlags = userFlags;
view.content.stripePortalUrl = stripePortalUrl();
view.jsFiles.push('zxcvbn');
view.cssFiles.push('index/user');

View File

@ -1,6 +1,12 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
interface TransactionInfo {
name: string;
index: number;
timestamp: Date;
}
// This transaction handler allows abstracting away the complexity of managing nested transactions
// within models.
// Any method in a model can start a transaction and, if one is already started, it
@ -9,7 +15,7 @@ import { DbConnection } from '../db';
// Set logEnabled_ to `true` to see what happens with nested transactions.
export default class TransactionHandler {
private transactionStack_: number[] = [];
private transactionStack_: TransactionInfo[] = [];
private activeTransaction_: Knex.Transaction = null;
private transactionIndex_: number = 0;
private logEnabled_: boolean = false;
@ -36,7 +42,15 @@ export default class TransactionHandler {
return this.activeTransaction_;
}
public async start(): Promise<number> {
public get stackInfo(): string {
const output: string[] = [];
for (const t of this.transactionStack_) {
output.push(`#${t.index}: ${t.name}: ${t.timestamp.toUTCString()}`);
}
return output.join('\n');
}
public async start(name: string): Promise<number> {
const txIndex = ++this.transactionIndex_;
this.log(`Starting transaction: ${txIndex}`);
@ -47,14 +61,19 @@ export default class TransactionHandler {
this.log(`Got transaction: ${txIndex}`);
}
this.transactionStack_.push(txIndex);
this.transactionStack_.push({
name,
index: txIndex,
timestamp: new Date(),
});
return txIndex;
}
private finishTransaction(txIndex: number): boolean {
if (!this.transactionStack_.length) throw new Error('Committing but no transaction was started');
const lastTxIndex = this.transactionStack_.pop();
if (lastTxIndex !== txIndex) throw new Error(`Committing a transaction but was not last to start one: ${txIndex}. Expected: ${lastTxIndex}`);
const lastTx = this.transactionStack_.pop();
if (lastTx.index !== txIndex) throw new Error(`Committing a transaction but was not last to start one: ${txIndex}. Expected: ${lastTx.index}`);
return !this.transactionStack_.length;
}

View File

@ -4,16 +4,20 @@
Most of your details can be found in your Profile page. To open it, click on the Profile button - this is the button in the top right corner, with your name or email on it.
## How can I cancel my account?
Click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Cancel subscription".
## How can I get more space?
If you are on a Basic account, you may upgrade to a Pro account to get more space. Click on the [Profile button](#how-can-i-change-my-details), then scroll down and select "Upgrade account".
If you are already on a Pro account, and you need more space for specific reasons, please contact us as we may increase the cap in some cases.
## How can I manage my payment details?
To update your card or other payment details, click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Manage payment details".
## How can I cancel my account?
Click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Cancel subscription".
## Further information
- [Joplin Offical Website](https://joplinapp.org)

View File

@ -131,6 +131,9 @@
{{#showUpdateSubscriptionPro}}
<a href="{{{global.baseUrl}}}/upgrade" class="button is-warning block">Upgrade to Pro</a>
{{/showUpdateSubscriptionPro}}
{{#showCancelSubscription}}
<p class="block"><a href="{{stripePortalUrl}}">Manage payment details</a></p>
{{/showCancelSubscription}}
{{#showCancelSubscription}}
<p id="user_cancel_subscription_link" class="block"><a href="#">Cancel subscription</a></p>
<input type="submit" id="user_cancel_subscription_button" name="user_cancel_subscription_button" class="button is-danger" value="Cancel subscription" />

5
packages/style.min.css vendored Normal file
View File

@ -0,0 +1,5 @@
.encryption-config-test > .item {
font-weight: bold;
}
/*# sourceMappingURL=style.min.css.map */

View File

@ -0,0 +1,45 @@
const sass = require('sass');
const fs = require('fs-extra');
// The SASS doc claims that renderSync is twice as fast as render, so if speed
// turns out to be an issue we could use that instead. The advantage of async is
// that we can run complation of each file in parallel (and running other async
// gulp tasks in parallel too).
// sasss.render is old school async, so convert it to a promise here.
async function sassRender(options) {
return new Promise((resolve, reject) => {
sass.render(options, ((error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}));
});
}
module.exports = async function compileSass(inputPaths, outputPath) {
const promises = [];
for (const inputPath of inputPaths) {
console.info(`Compiling ${inputPath}...`);
promises.push(sassRender({
file: inputPath,
sourceMap: true,
outFile: outputPath,
}));
}
const results = await Promise.all(promises);
const cssString = results.map(r => r.css.toString()).join('\n');
const mapString = results.map(r => r.map.toString()).join('\n');
await Promise.all([
fs.writeFile(outputPath, cssString, 'utf8'),
fs.writeFile(`${outputPath}.map`, mapString, 'utf8'),
]);
console.info(`Generated ${outputPath}`);
};

View File

@ -7,6 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 12.09.2021\n"
"Last-Translator: mrkaato\n"
"Language-Team: \n"
"Language: fi_FI\n"
@ -14,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.4.1\n"
"X-Generator: Poedit 3.0\n"
#: packages/app-cli/app/app-gui.js:452
msgid "To delete a tag, untag the associated notes."
@ -133,6 +135,7 @@ msgid ""
"Runs the commands contained in the text file. There should be one command "
"per line."
msgstr ""
"Suorittaa tekstitiedoston komennot. Riviä kohden pitäisi olla yksi komento."
#: packages/app-cli/app/command-cat.js:14
msgid "Displays the given note."
@ -599,8 +602,8 @@ msgid ""
"\n"
"%s"
msgstr ""
"Sets the property <name> of the given <note> to the given [value]. Possible "
"properties are:\n"
"Asettaa ominaisuuden <name> annetusta <note> annettuun [arvoon]. Mahdolliset "
"ominaisuudet ovat:\n"
"\n"
"%s"
@ -647,7 +650,7 @@ msgstr ""
#: packages/app-desktop/gui/DropboxLoginScreen.js:30
#: packages/app-mobile/components/screens/dropbox-login.js:59
msgid "Step 1: Open this URL in your browser to authorise the application:"
msgstr "Vaihe 1: Avaa tämä URL-osoite selaimessa valtuuttaaksesi sovelluksen:"
msgstr "Vaihe 1: Avaa tämä URL osoite selaimessa valtuuttaaksesi sovelluksen:"
#: packages/app-cli/app/command-sync.js:94
#: packages/app-desktop/gui/DropboxLoginScreen.js:32
@ -908,11 +911,11 @@ msgstr "Lataa"
#: packages/app-desktop/checkForUpdates.js:189
msgid "Skip this version"
msgstr ""
msgstr "Ohita tämä versio"
#: packages/app-desktop/checkForUpdates.js:189
msgid "Full changelog"
msgstr ""
msgstr "Täysi muutosloki"
#: packages/app-desktop/commands/copyDevCommand.js:18
msgid "Copy dev mode command to clipboard"
@ -939,9 +942,8 @@ msgid "Stop"
msgstr "Seis"
#: packages/app-desktop/commands/toggleSafeMode.js:18
#, fuzzy
msgid "Toggle safe mode"
msgstr "Näytä sivupalkki"
msgstr "Vaihda turvalliseen tilaan"
#: packages/app-desktop/gui/ClipperConfigScreen.js:34
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:40
@ -951,7 +953,7 @@ msgstr "Tunnus on kopioitu leikepöydälle!"
#: packages/app-desktop/gui/ClipperConfigScreen.js:37
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:44
msgid "Are you sure you want to renew the authorisation token?"
msgstr ""
msgstr "Haluatko varmasti uusia valtuutustunnuksen?"
#: packages/app-desktop/gui/ClipperConfigScreen.js:67
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:84
@ -1053,7 +1055,7 @@ msgstr ""
#: packages/app-desktop/gui/ClipperConfigScreen.js:111
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:222
msgid "Renew token"
msgstr ""
msgstr "Uusi tunnus"
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.js:27
msgid "Apply"
@ -1179,13 +1181,12 @@ msgid "You do not have any installed plugin."
msgstr "Sinulla ei ole asennettua laajennusta."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:232
#, fuzzy
msgid "Could not connect to plugin repository"
msgstr "Laajennuksen asentaminen epäonnistui: %s"
msgstr "Yhteyden muodostaminen laajennusten arkistoon epäonnistui"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:234
msgid "Try again"
msgstr ""
msgstr "Yritä uudestaan"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:242
msgid "Plugin tools"
@ -1219,19 +1220,16 @@ msgid "Submit"
msgstr "Lähetä"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:73
#, fuzzy
msgid "Source: "
msgstr "Lähde"
msgstr "Lähde: "
#: packages/app-desktop/gui/EncryptionConfigScreen.js:76
#, fuzzy
msgid "Created: "
msgstr "Luotu: %s"
msgstr "Luotu: "
#: packages/app-desktop/gui/EncryptionConfigScreen.js:79
#, fuzzy
msgid "Updated: "
msgstr "Päivitetty: %s"
msgstr "Päivitetty: "
#: packages/app-desktop/gui/EncryptionConfigScreen.js:84
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:96
@ -1241,9 +1239,8 @@ msgid "Save"
msgstr "Tallenna"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:87
#, fuzzy
msgid "Disable"
msgstr "Poistettu käytöstä"
msgstr "Poista käytöstä"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:87
#: packages/app-mobile/components/screens/encryption-config.js:157
@ -1355,11 +1352,11 @@ msgstr "Pääavaimet"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:142
msgid "Hide disabled master keys"
msgstr ""
msgstr "Piilota käytöstä poistetut pääavaimet"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:142
msgid "Show disabled master keys"
msgstr ""
msgstr "Näytä käytöstä poistetut pääavaimet"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:143
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:343
@ -1380,7 +1377,7 @@ msgstr "Aktiivinen"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:149
msgid "Date"
msgstr ""
msgstr "Päivämäärä"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:150
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:329
@ -1388,14 +1385,12 @@ msgid "Password"
msgstr "Salasana"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:151
#, fuzzy
msgid "Valid"
msgstr "Virheellinen"
msgstr "Kelvollinen"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:152
#, fuzzy
msgid "Actions"
msgstr "Toiminta"
msgstr "Toiminnot"
#: packages/app-desktop/gui/EncryptionConfigScreen.js:182
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:244
@ -1575,12 +1570,12 @@ msgstr "Sulje ikkuna"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:24
msgid "Preferences"
msgstr "Asetukset"
msgstr "Oletusasetukset"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:24
#: packages/app-desktop/gui/MenuBar.js:310 packages/app-desktop/gui/Root.js:163
msgid "Options"
msgstr "Vaihtoehdot"
msgstr "Asetukset"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:31
msgid "Invalid"
@ -1591,10 +1586,12 @@ msgid ""
"Safe mode is currently active. Note rendering and all plugins are "
"temporarily disabled."
msgstr ""
"Turvallinen tila on tällä hetkellä aktiivinen. Huomautusten renderöinti ja "
"kaikki laajennukset on tilapäisesti poistettu käytöstä."
#: packages/app-desktop/gui/MainScreen/MainScreen.js:470
msgid "Disable safe mode and restart"
msgstr ""
msgstr "Lopeta turvallinen tila ja käynnistä uudelleen"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:473
msgid ""
@ -1638,16 +1635,16 @@ msgstr "Lisätietoja"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:487
#, javascript-format
msgid "%s (%s) would like to share a notebook with you."
msgstr ""
msgstr "%s (%s) haluaa jakaa muistikirjan kanssasi."
#: packages/app-desktop/gui/MainScreen/MainScreen.js:487
msgid "Accept"
msgstr ""
msgstr "Hyväksy"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:487
#: packages/app-desktop/gui/Root.js:121
msgid "Reject"
msgstr ""
msgstr "Hylkää"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:490
msgid "Some items cannot be synchronised."
@ -1666,9 +1663,8 @@ msgid "Use the arrows to move the layout items. Press \"Escape\" to exit."
msgstr "Siirrä asettelukohteita nuolilla. Paina \"Esc\" poistuaksesi."
#: packages/app-desktop/gui/MainScreen/commands/commandPalette.js:18
#, fuzzy
msgid "Command palette..."
msgstr "Komentovalikoima"
msgstr "Komentovalikoima..."
#: packages/app-desktop/gui/MainScreen/commands/editAlarm.js:20
#: packages/app-mobile/components/SelectDateTimeDialog.js:84
@ -1683,7 +1679,7 @@ msgstr "Aseta hälytys:"
#: packages/app-desktop/gui/MainScreen/commands/exportPdf.js:20
#: packages/app-desktop/gui/MainScreen/commands/exportPdf.js:32
msgid "PDF File"
msgstr "PDF-tiedosto"
msgstr "PDF tiedosto"
#: packages/app-desktop/gui/MainScreen/commands/gotoAnything.js:23
#: packages/app-desktop/plugins/GotoAnything.js:497
@ -1766,14 +1762,12 @@ msgid "Note properties"
msgstr "Muistiinpanon ominaisuudet"
#: packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js:16
#, fuzzy
msgid "Share notebook..."
msgstr "Muistiinpanon jakaminen..."
msgstr "Muistikirjan jakaminen..."
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js:16
#, fuzzy
msgid "Publish note..."
msgstr "Muistiinpanon jakaminen..."
msgstr "Muistiinpanon julkaiseminen..."
#: packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js:19
#: packages/lib/services/spellChecker/SpellCheckerService.js:180
@ -1833,7 +1827,7 @@ msgstr "Joplinista"
#: packages/app-desktop/gui/MenuBar.js:367
msgid "Preferences..."
msgstr "Asetukset..."
msgstr "Oletusasetukset..."
#: packages/app-desktop/gui/MenuBar.js:377
#: packages/app-desktop/gui/MenuBar.js:618
@ -1861,7 +1855,7 @@ msgstr "&Näytä"
#: packages/app-desktop/gui/MenuBar.js:506
msgid "Layout button sequence"
msgstr "Asettele painike järjestys"
msgstr "Aseta painike järjestys"
#: packages/app-desktop/gui/MenuBar.js:551
#: packages/app-desktop/gui/MenuBar.js:557
@ -1882,12 +1876,11 @@ msgstr "&Mene"
#: packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js:18
#: packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js:18
msgid "Focus"
msgstr "Keskittää"
msgstr "Osoita"
#: packages/app-desktop/gui/MenuBar.js:585
#, fuzzy
msgid "Note&book"
msgstr "Muistikirjat"
msgstr "&Muistikirjat"
#: packages/app-desktop/gui/MenuBar.js:591
msgid "&Note"
@ -2049,23 +2042,23 @@ msgstr "Muokkaa"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:17
msgid "Highlight"
msgstr ""
msgstr "Korosta"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:22
msgid "Strikethrough"
msgstr ""
msgstr "Yliviivaus"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:27
msgid "Insert"
msgstr ""
msgstr "Aseta"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:33
msgid "Superscript"
msgstr ""
msgstr "Yläindeksi"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:39
msgid "Subscript"
msgstr ""
msgstr "Alaindeksi"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:285
msgid "Click to add tags..."
@ -2162,9 +2155,8 @@ msgid "Delete line"
msgstr "Poista rivi"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
#, fuzzy
msgid "Duplicate line"
msgstr "Duplikaatti, toinen samanlainen"
msgstr "Rivin kaksoiskappale"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
msgid "Undo"
@ -2372,18 +2364,16 @@ msgstr ""
"Varoitus: kaikkia resursseja ei näytetä suorituskyvyn vuoksi (raja: %s)."
#: packages/app-desktop/gui/Root.js:106
#, fuzzy
msgid "Confirmation"
msgstr "Konfigurointi"
msgstr "Vahvistus"
#: packages/app-desktop/gui/Root.js:119
msgid "The Web Clipper needs your authorisation to access your data."
msgstr ""
msgstr "Web Clipper tarvitsee valtuutuksen tietojen käyttöön."
#: packages/app-desktop/gui/Root.js:120
#, fuzzy
msgid "Grant authorisation"
msgstr "Valtuutuksen tunnus:"
msgstr "Myönnä lupa"
#: packages/app-desktop/gui/Root.js:160
msgid "OneDrive Login"
@ -2398,19 +2388,20 @@ msgid "Note attachments"
msgstr "Muistiinpanon liitteet"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:141
#, fuzzy
msgid "Unshare"
msgstr "Jaa"
msgstr "Poista jakaminen"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:183
msgid ""
"Delete this invitation? The recipient will no longer have access to this "
"shared notebook."
msgstr ""
"Poistetaanko tämä kutsu? Vastaanottaja ei voi enää käyttää tätä jaettua "
"muistikirjaa."
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:197
msgid "Add recipient:"
msgstr ""
msgstr "Lisää vastaanottaja:"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:200
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:28
@ -2420,45 +2411,43 @@ msgstr "Jaa"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:209
msgid "Recipient has not yet accepted the invitation"
msgstr ""
msgstr "Vastaanottaja ei ole vielä hyväksynyt kutsua"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:210
msgid "Recipient has rejected the invitation"
msgstr ""
msgstr "Vastaanottaja on hylännyt kutsun"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:211
msgid "Recipient has accepted the invitation"
msgstr ""
msgstr "Vastaanottaja on hyväksynyt kutsun"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:221
msgid "Recipients:"
msgstr ""
msgstr "Vastaanottajat:"
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:233
#, fuzzy
msgid "Synchronizing..."
msgstr "Synkronoidaan..."
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:234
#, fuzzy
msgid "Sharing notebook..."
msgstr "Muistiinpanon jakaminen..."
msgstr "Jaetaan muistikirjaa..."
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:244
msgid ""
"Unshare this notebook? The recipients will no longer have access to its "
"content."
msgstr ""
"Poistetaanko tämän muistikirjan jakaminen? Vastaanottajilla ei ole enää "
"pääsyä sen sisältöön."
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:254
#, fuzzy
msgid "Share Notebook"
msgstr "Jaa muistiinpanoja"
msgstr "Jaa muistikirja"
#: packages/app-desktop/gui/ShareNoteDialog.js:144
#, fuzzy
msgid "Unpublish note"
msgstr "Jaa"
msgstr "Peruuta muistiinpanon julkaisu"
#: packages/app-desktop/gui/ShareNoteDialog.js:171
msgid "Synchronising..."
@ -2484,7 +2473,7 @@ msgstr ""
#: packages/app-desktop/gui/ShareNoteDialog.js:187
msgid "Publish Notes"
msgstr ""
msgstr "Julkaise muistiinpanot"
#: packages/app-desktop/gui/ShareNoteDialog.js:189
msgid "Copy Shareable Link"
@ -2565,32 +2554,28 @@ msgid "Retry"
msgstr "Yritä uudelleen"
#: packages/app-desktop/gui/StatusScreen/StatusScreen.js:137
#, fuzzy
msgid "Advanced tools"
msgstr "Lisäasetukset"
msgstr "Lisätyökalut"
#: packages/app-desktop/gui/StatusScreen/StatusScreen.js:139
#, fuzzy
msgid "Export debug report"
msgstr "Vie virheenkorjausraportti"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:157
#, fuzzy
msgid "Sync your notes"
msgstr "Lajittele muistiinpanot"
msgstr "Synkronoi muistiinpanosi"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:158
msgid "Publish notes to the internet"
msgstr ""
msgstr "Julkaise muistiinpanot Internetissä"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:159
#, fuzzy
msgid "Collaborate on notebooks with others"
msgstr "Luo ensin muistikirja"
msgstr "Muistikirjojen yhteiskäyttö muiden kanssa"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:182
msgid "Thank you! Your Joplin Cloud account is now setup and ready to use."
msgstr ""
msgstr "Kiitos! Your Joplin Cloud tilisi on nyt määritetty ja käyttövalmis."
#: packages/app-desktop/gui/SyncWizard/Dialog.js:190
#, javascript-format
@ -2600,30 +2585,34 @@ msgid ""
"\n"
"%s"
msgstr ""
"Joplin Cloud tilisi määrittämisessä tapahtui virhe. Vahvista "
"sähköpostiosoitteesi ja salasanasi sekä yritä uudelleen. Virhe oli:\n"
"\n"
"%s"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:203
msgid "Login below."
msgstr ""
msgstr "Kirjaudu sisään alla."
#: packages/app-desktop/gui/SyncWizard/Dialog.js:205
#, fuzzy
msgid "Or create an account."
msgstr "Luo uuden muistiinpanon."
msgstr "Tai luo tili."
#: packages/app-desktop/gui/SyncWizard/Dialog.js:210
msgid "Login"
msgstr ""
msgstr "Kirjaudu"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:231
#, fuzzy
msgid "Select"
msgstr "Valitse kaikki"
msgstr "Valitse"
#: packages/app-desktop/gui/SyncWizard/Dialog.js:278
msgid ""
"Joplin can synchronise your notes using various providers. Select one from "
"the list below."
msgstr ""
"Joplin voi synkronoida muistiinpanosi eri palveluntarjoajien avulla. Valitse "
"yksi alla olevasta luettelosta."
#: packages/app-desktop/gui/utils/NoteListUtils.js:43
msgid "Duplicate"
@ -2644,7 +2633,7 @@ msgstr "Vaihda muistiinpanotyyppiin"
#: packages/app-desktop/gui/utils/NoteListUtils.js:93
msgid "Switch to to-do type"
msgstr "Siirry tehtävätyyppiin"
msgstr "Vaihda tehtävätyyppiin"
#: packages/app-desktop/gui/utils/NoteListUtils.js:100
#: packages/app-mobile/components/screens/Note.js:847
@ -2797,7 +2786,7 @@ msgstr "Vain virheenkorjausta varten: vie profiilisi ulkoiselle SD-kortille."
#: packages/app-mobile/components/screens/ConfigScreen.js:436
msgid "Feature flags"
msgstr ""
msgstr "Ominaisuus liput"
#: packages/app-mobile/components/screens/ConfigScreen.js:439
msgid "More information"
@ -2809,8 +2798,8 @@ msgid ""
"them in your phone settings, in Apps > Joplin > Permissions"
msgstr ""
"Toimiakseen oikein sovellus tarvitsee seuraavat käyttöoikeudet. Ota ne "
"käyttöön puhelimesi asetuksissa, valitsemalla Sovellukset> Joplin> "
"Käyttöoikeudet"
"käyttöön puhelimesi asetuksissa, valitsemalla Sovellukset> Joplin> Luvat "
"(käyttöoikeudet)"
#: packages/app-mobile/components/screens/ConfigScreen.js:446
msgid ""
@ -2822,11 +2811,11 @@ msgstr ""
#: packages/app-mobile/components/screens/ConfigScreen.js:447
msgid "- Camera: to allow taking a picture and attaching it to a note."
msgstr "- Kamera: sallii kuvan ottamisen ja liittämisen muistiinpanoon."
msgstr "- Kamera: sallia kuvan ottamisen ja liittämisen muistiinpanoon."
#: packages/app-mobile/components/screens/ConfigScreen.js:448
msgid "- Location: to allow attaching geo-location information to a note."
msgstr "- Sijainti: sallii paikkatietojen liittämisen muistiinpanoon."
msgstr "- Sijainti: sallia paikkatietojen liittämisen muistiinpanoon."
#: packages/app-mobile/components/screens/ConfigScreen.js:459
msgid "Joplin website"
@ -2931,7 +2920,7 @@ msgstr "Näytä kartalla"
#: packages/app-mobile/components/screens/Note.js:761
msgid "Go to source URL"
msgstr "Siirry lähteen URL-osoitteeseen"
msgstr "Siirry lähteen URL osoitteeseen"
#: packages/app-mobile/components/screens/Note.js:790
msgid "Attach..."
@ -3048,7 +3037,7 @@ msgstr "Uusi muistikirja"
#: packages/app-mobile/components/side-menu-content.js:351
msgid "Mobile data - auto-sync disabled"
msgstr ""
msgstr "Mobiilidata - automaattinen synkronointi poistettu käytöstä"
#: packages/lib/BaseApplication.js:154 packages/lib/BaseApplication.js:166
#: packages/lib/BaseApplication.js:198
@ -3070,7 +3059,7 @@ msgid ""
"%s"
msgstr ""
"Yhteyden muodostaminen Joplinin palvelimelle epäonnistui. Tarkista "
"vaihtoehdot Synkronointiasetukset-näytössä. Koko virhe oli:\n"
"synkronoinnin asetukset. Koko virhe oli:\n"
"\n"
"%s"
@ -3087,15 +3076,17 @@ msgid "File system"
msgstr "Tiedostojärjestelmä"
#: packages/lib/SyncTargetJoplinCloud.js:28
#, fuzzy
msgid "Joplin Cloud"
msgstr "Joplin Foorumi"
msgstr "Joplin Cloud"
#: packages/lib/SyncTargetJoplinCloud.js:31
msgid ""
"Joplin's own sync service. Also gives access to Joplin-specific features "
"such as publishing notes or collaborating on notebooks with others."
msgstr ""
"Joplinin oma synkronointipalvelu. Voit myös käyttää Joplin "
"erityisominaisuuksia, kuten muistiinpanojen julkaisemista tai muistikirjojen "
"yhteiskäyttöä muiden kanssa."
#: packages/lib/SyncTargetJoplinServer.js:60
msgid "Joplin Server"
@ -3107,7 +3098,7 @@ msgstr "Nextcloud"
#: packages/lib/SyncTargetNone.js:22
msgid "(None)"
msgstr ""
msgstr "(Ei mitään)"
#: packages/lib/SyncTargetOneDrive.js:32
msgid "OneDrive"
@ -3157,9 +3148,9 @@ msgid "Cancelling..."
msgstr "Peruutetaan..."
#: packages/lib/Synchronizer.js:159
#, fuzzy, javascript-format
#, javascript-format
msgid "Completed: %s (%s)"
msgstr "Valmis: %s"
msgstr "Valmis: %s (%s)"
#: packages/lib/Synchronizer.js:161
#, javascript-format
@ -3196,8 +3187,8 @@ msgid ""
"Error. Please check that URL, username, password, etc. are correct and that "
"the sync target is accessible. The reported error was:"
msgstr ""
"Virhe. Tarkista URL-osoite, käyttäjänimi, salasana jne. ovat oikeita ja "
"synkronointikohde on käytettävissä. Raportoitu virhe oli:"
"Virhe. Tarkista URL osoite, käyttäjänimi, salasana jne. ovat oikeita ja "
"synkronointi kohde on käytettävissä. Raportoitu virhe oli:"
#: packages/lib/components/shared/dropbox-login-shared.js:39
msgid "The application has been authorised!"
@ -3252,7 +3243,7 @@ msgstr "Purettuja kohteita: %s / %s"
#: packages/lib/components/shared/encryption-config-shared.js:151
#, javascript-format
msgid "Encryption will be enabled using the master key created on %s"
msgstr ""
msgstr "Salaus otetaan käyttöön käyttämällä luotua pääavainta %s"
#: packages/lib/models/BaseItem.js:721
msgid "Encrypted"
@ -3322,9 +3313,8 @@ msgid "Error"
msgstr "Virhe"
#: packages/lib/models/Resource.js:408
#, fuzzy
msgid "Conflicts (attachments)"
msgstr "Muistiinpanon liitteet"
msgstr "Ristiriidat (liitteet)"
#: packages/lib/models/Resource.js:422
#, javascript-format
@ -3400,7 +3390,7 @@ msgstr "OLED Dark"
#: packages/lib/models/Setting.js:152
msgid "Open Sync Wizard..."
msgstr ""
msgstr "Avaa ohjattu synkronointitoiminto."
#: packages/lib/models/Setting.js:162
msgid "Synchronisation target"
@ -3464,23 +3454,20 @@ msgid "Joplin Server URL"
msgstr "Joplin palvelimen URL-osoite"
#: packages/lib/models/Setting.js:348
#, fuzzy
msgid "Joplin Server email"
msgstr "Joplin Palvelin"
msgstr "Joplin palvelimen sähköposti"
#: packages/lib/models/Setting.js:359
msgid "Joplin Server password"
msgstr "Joplin palvelimen salasana"
#: packages/lib/models/Setting.js:386
#, fuzzy
msgid "Joplin Cloud email"
msgstr "Joplin Palvelin"
msgstr "Joplin Cloud sähköposti"
#: packages/lib/models/Setting.js:397
#, fuzzy
msgid "Joplin Cloud password"
msgstr "Joplin palvelimen salasana"
msgstr "Joplin Cloud salasana"
#: packages/lib/models/Setting.js:409
msgid "Attachment download behaviour"
@ -3492,8 +3479,8 @@ msgid ""
"In \"Auto\", they are downloaded when you open the note. In \"Always\", all "
"the attachments are downloaded whether you open the note or not."
msgstr ""
"Manuaalisessa tilassa liitteet ladataan vain, kun napsautat niitä. \"Auto\" -"
"kohdassa ne ladataan, kun avaat muistiinpanon. \"Aina\" -kohdassa kaikki "
"Manuaalisessa tilassa liitteet ladataan vain, kun napsautat niitä. \"Auto\" "
"valinnassa ne ladataan, kun avaat muistiinpanon. \"Aina\" valinnassa kaikki "
"liitteet ladataan riippumatta siitä, avaatko muistiinpanon vai et."
#: packages/lib/models/Setting.js:413
@ -3720,11 +3707,12 @@ msgid ""
"Used for most text in the markdown editor. If not found, a generic "
"proportional (variable width) font is used."
msgstr ""
"Käytetään useimmissa teksteissä markdown editorissa. Jos sitä ei löydy, "
"käytetään yleistä suhteellista fonttia (vaihtelevaa leveyttä)."
#: packages/lib/models/Setting.js:779
#, fuzzy
msgid "Editor monospace font family"
msgstr "Editorin fonttiperhe"
msgstr "Editorin monospace fonttiperhe"
#: packages/lib/models/Setting.js:780
msgid ""
@ -3732,14 +3720,17 @@ msgid ""
"tables, checkboxes, code). If not found, a generic monospace (fixed width) "
"font is used."
msgstr ""
"Käytetään, kun tekstiin tarvitaan kiinteäleveyksinen fontti (esim. taulukot, "
"valintaruudut, koodi). Jos sitä ei löydy, käytetään yleistä monospace "
"fonttia (kiinteäleveyksinen)."
#: packages/lib/models/Setting.js:783
msgid "Editor maximum width"
msgstr ""
msgstr "Editorin enimmäisleveys"
#: packages/lib/models/Setting.js:783
msgid "Set it to 0 to make it take the complete available space."
msgstr ""
msgstr "Aseta arvoksi 0, jotta se vie koko käytettävissä olevan tilan."
#: packages/lib/models/Setting.js:802
msgid "Custom stylesheet for rendered Markdown"
@ -3751,11 +3742,11 @@ msgstr "Mukautettu tyylitaulukko Joplinin sovellustyyleille"
#: packages/lib/models/Setting.js:828
msgid "Re-upload local data to sync target"
msgstr ""
msgstr "Lataa paikalliset tiedot uudelleen kohteen synkronointia varten"
#: packages/lib/models/Setting.js:838
msgid "Delete local data and re-download from sync target"
msgstr ""
msgstr "Poista paikalliset tiedot ja lataa uudelleen synkronointikohteesta"
#: packages/lib/models/Setting.js:843
msgid "Automatically update the application"
@ -3792,7 +3783,7 @@ msgstr "%d tuntia"
#: packages/lib/models/Setting.js:871
msgid "Synchronise only over WiFi connection"
msgstr ""
msgstr "Synkronoi vain WiFi yhteyden kautta"
#: packages/lib/models/Setting.js:878
msgid "Text editor command"
@ -3809,7 +3800,7 @@ msgstr ""
#: packages/lib/models/Setting.js:879
msgid "Page size for PDF export"
msgstr "PDF-viennin sivukoko"
msgstr "PDF viennin sivukoko"
#: packages/lib/models/Setting.js:881
msgid "A4"
@ -3837,15 +3828,15 @@ msgstr "Legal"
#: packages/lib/models/Setting.js:889
msgid "Page orientation for PDF export"
msgstr "Sivun suunta PDF-vientiä varten"
msgstr "Sivun suunta PDF vientiä varten"
#: packages/lib/models/Setting.js:891
msgid "Portrait"
msgstr "Pysty"
msgstr "Pystysuunta"
#: packages/lib/models/Setting.js:892
msgid "Landscape"
msgstr "Vaaka"
msgstr "Vaakasuunta"
#: packages/lib/models/Setting.js:902
msgid "Keyboard Mode"
@ -3861,11 +3852,11 @@ msgstr "Vim"
#: packages/lib/models/Setting.js:920
msgid "Do not resize images"
msgstr ""
msgstr "Älä muuta kuvien kokoa"
#: packages/lib/models/Setting.js:935
msgid "Custom TLS certificates"
msgstr "Mukautetut TLS-varmenteet"
msgstr "Mukautetut TLS varmenteet"
#: packages/lib/models/Setting.js:936
msgid ""
@ -3876,13 +3867,13 @@ msgid ""
msgstr ""
"Pilkuilla erotettu luettelo hakemistoiden poluista, joilta varmenteet "
"ladataan, tai polku yksittäisiin varmennetiedostoihin. Esimerkiksi: /my/"
"cert_dir, /other/custom.pem. Huomaa, että jos teet muutoksia TLS-asetuksiin, "
"cert_dir, /other/custom.pem. Huomaa, että jos teet muutoksia TLS asetuksiin, "
"sinun on tallennettava muutokset ennen kuin napsautat \"Tarkista "
"synkronointiasetukset\"."
#: packages/lib/models/Setting.js:958
msgid "Ignore TLS certificate errors"
msgstr "Ohita TLS-varmenteen virheet"
msgstr "Ohita TLS varmenteen virheet"
#: packages/lib/models/Setting.js:967
msgid "Fail-safe"
@ -3893,15 +3884,15 @@ msgid ""
"Fail-safe: Do not wipe out local data when sync target is empty (often the "
"result of a misconfiguration or bug)"
msgstr ""
"Vikasietoinen: Älä pyyhi paikallisia tietoja, kun synkronointikohde on tyhjä "
"(usein virheellisen määritysvirheen tai virheen seurauksena)"
"Vikaturvallinen: Älä pyyhi paikallisia tietoja, kun synkronointikohde on "
"tyhjä (usein virheellisen määritysvirheen tai virheen seurauksena)"
#: packages/lib/models/Setting.js:972
msgid ""
"Specify the port that should be used by the API server. If not set, a "
"default will be used."
msgstr ""
"Määritä portti, jota API-palvelimen on käytettävä. Jos sitä ei ole "
"Määritä portti, jota API palvelimen on käytettävä. Jos sitä ei ole "
"määritetty, käytetään oletusarvoa."
#: packages/lib/models/Setting.js:977
@ -3933,10 +3924,10 @@ msgid ""
"item with a factor of 2 will take twice as much space as an item with a "
"factor of 1.Restart app to see changes."
msgstr ""
"Kerroin-ominaisuus määrittää, miten kohde kasvaa tai kutistuu niin, että se "
"Kerroin ominaisuus määrittää, miten kohde kasvaa tai kutistuu niin, että se "
"sopii säiliössä olevaan tilaan suhteessa muihin kohteisiin. Näin ollen "
"kohde, jonka kerroin on 2, vie kaksi kertaa enemmän tilaa kuin kohde, jonka "
"kerroin on 1.Restart-sovellus muutosten näkemistä varten."
"kerroin on 1.Restart sovellus muutosten näkemistä varten."
#: packages/lib/models/Setting.js:1029
msgid "Note list growth factor"
@ -4010,7 +4001,7 @@ msgstr ""
#: packages/lib/models/Setting.js:1709
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Muistiinpanot ja asetukset tallennetaan: %s"
msgstr "Muistiinpanot ja oletusasetukset tallennetaan: %s"
#: packages/lib/models/Tag.js:223
#, javascript-format
@ -4213,28 +4204,28 @@ msgstr "Muistiinpano \"%s\" on palautettu muistikirjaan \"%s\"."
#: packages/lib/services/interop/InteropService.js:48
#: packages/lib/services/interop/InteropService.js:57
msgid "Joplin Export File"
msgstr "Joplin Vie tiedosto"
msgstr "Joplin vie tiedosto"
#: packages/lib/services/interop/InteropService.js:50
#: packages/lib/services/interop/InteropService.js:58
msgid "Joplin Export Directory"
msgstr "Joplin Vie hakemisto"
msgstr "Joplin vie hakemisto"
#: packages/lib/services/interop/InteropService.js:51
msgid "Evernote Export File (as Markdown)"
msgstr "Evernote Vie tiedosto (kuten Markdown)"
msgstr "Evernote vie tiedosto (kuten Markdown)"
#: packages/lib/services/interop/InteropService.js:52
msgid "Evernote Export File (as HTML)"
msgstr "Evernote Vie tiedosto (kuten HTML)"
msgstr "Evernote vie tiedosto (kuten HTML)"
#: packages/lib/services/interop/InteropService.js:60
msgid "HTML File"
msgstr "HTML Tiedosto"
msgstr "HTML tiedosto"
#: packages/lib/services/interop/InteropService.js:61
msgid "HTML Directory"
msgstr "HTML Hakemisto"
msgstr "HTML hakemisto"
#: packages/lib/services/interop/InteropService.js:127
#, javascript-format
@ -4324,19 +4315,19 @@ msgid ""
"The default admin password is insecure and has not been changed! [Change it "
"now](%s)"
msgstr ""
"Järjestelmänvalvojan oletussalasana on epävarma, eikä sitä ole muutettu "
"[Vaihda se nyt] (%s)"
"Järjestelmänvalvojan oletussalasana on turvaton, eikä sitä ole muutettu "
"[Vaihda se nyt](%s)"
#: packages/server/dist/models/UserModel.js:199
#: packages/server/dist/models/UserModel.js:204
#, fuzzy
msgid "attachment"
msgstr "Liitteet"
msgstr "liite"
#: packages/server/dist/models/UserModel.js:199
#, javascript-format
msgid "Cannot save %s \"%s\" because it is larger than the allowed limit (%s)"
msgstr ""
"Tallennus ei onnistu %s \"%s\" koska se on suurempi kuin sallittu raja (%s)"
#: packages/server/dist/models/UserModel.js:204
#, javascript-format
@ -4344,6 +4335,8 @@ msgid ""
"Cannot save %s \"%s\" because it would go over the total allowed size (%s) "
"for this account"
msgstr ""
"Kohdetta %s \"%s\" ei voida tallentaa, koska se ylittäisi tämän tilin "
"sallitun kokonaiskoon (%s)"
#, javascript-format
#~ msgid "%s %s (%s)"
@ -4370,8 +4363,8 @@ msgstr ""
#~ msgid "Templates"
#~ msgstr "Mallit"
#~ msgid "Share Notes"
#~ msgstr "Jaa muistiinpanoja"
#~ msgid "Full Release Notes"
#~ msgstr "Täydelliset julkaisutiedot"
#~ msgid "Joplin Server Directory"
#~ msgstr "Joplin palvelimen hakemisto"
@ -4379,25 +4372,11 @@ msgstr ""
#~ msgid "Joplin Server username"
#~ msgstr "Joplin palvelimen käyttäjänimi"
#, fuzzy
#~ msgid "marked text"
#~ msgstr "korostettu teksti"
#, fuzzy
#~ msgid "Mark"
#~ msgstr "Merkintä"
#~ msgid "Full Release Notes"
#~ msgstr "Täydelliset julkaisutiedot"
#, fuzzy
#~ msgid ""
#~ "If the font is incorrect or empty, it will default to a generic monospace "
#~ "font."
#~ msgstr ""
#~ "Tämän on oltava *monospace* fontti, muuten se ei toimi oikein. Jos "
#~ "kirjasin on väärä tai tyhjä, se on oletusarvoisesti yleinen monospace "
#~ "fontti."
#~ "Jos fontti on väärä tai tyhjä, se on oletuksena yleinen monospace fontti."
#~ msgid ""
#~ "This should be a *monospace* font or some elements will render "

View File

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "@joplin/tools",
"version": "2.3.0",
"version": "2.4.1",
"license": "MIT",
"dependencies": {
"execa": "^4.1.0",
@ -31,6 +31,7 @@
"@types/mustache": "^0.8.32",
"@types/node": "^14.14.6",
"gulp": "^4.0.2",
"sass": "^1.39.2",
"sqlite3": "^5.0.0",
"typescript": "^4.1.3"
}
@ -4197,6 +4198,18 @@
"integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==",
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@ -4654,6 +4667,159 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
"version": "1.39.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.39.2.tgz",
"integrity": "sha512-4/6Vn2RPc+qNwSclUSKvssh7dqK1Ih3FfHBW16I/GfH47b3scbYeOw65UIrYG7PkweFiKbpJjgkf5CV8EMmvzw==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=8.9.0"
}
},
"node_modules/sass/node_modules/anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/sass/node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/sass/node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/sass/node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sass/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/sass/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/sass/node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sass/node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/sass/node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -9271,6 +9437,12 @@
"integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==",
"dev": true
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@ -9642,6 +9814,116 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
"version": "1.39.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.39.2.tgz",
"integrity": "sha512-4/6Vn2RPc+qNwSclUSKvssh7dqK1Ih3FfHBW16I/GfH47b3scbYeOw65UIrYG7PkweFiKbpJjgkf5CV8EMmvzw==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
},
"dependencies": {
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",

View File

@ -42,6 +42,7 @@
"@types/mustache": "^0.8.32",
"@types/node": "^14.14.6",
"gulp": "^4.0.2",
"sass": "^1.39.2",
"sqlite3": "^5.0.0",
"typescript": "^4.1.3"
},

View File

@ -79,6 +79,14 @@
{
"name": "cuongtransc",
"id": "808091"
},
{
"name": "iamwillbar",
"id": "3266447"
},
{
"name": "marcdw1289",
"id": "42319182"
}
],
"orgs": [