1
0
mirror of https://github.com/keeweb/keeweb.git synced 2024-06-20 06:56:40 +02:00

Merge branch 'develop' into usb

# Conflicts:
#	app/scripts/models/app-settings-model.js
#	desktop/app.js
This commit is contained in:
antelle 2020-04-18 11:03:06 +02:00
commit 66b2b77791
No known key found for this signature in database
GPG Key ID: 094A2F2D6136A4EE
39 changed files with 739 additions and 657 deletions

View File

@ -411,11 +411,19 @@ async function run() {
const regex = new RegExp(
`#####\\s+v${version.replace(/\./g, '\\.')}.*?\n([\\s\\S]*?)\n#####`
);
const body = releaseNotes
const bodyReleaseNotes = releaseNotes
.match(regex)[1]
.trim()
.replace(/\s*\n/g, '\n');
const bodyTemplate =
'{release_notes}\n\n' +
'Want to keep releases happening? ' +
'Donate to KeeWeb on [OpenCollective](https://opencollective.com/keeweb). ' +
'Thank you!';
const body = bodyTemplate.replace('{release_notes}', bodyReleaseNotes);
console.log(`Updating release with notes:\n${body}`);
await github.repos.updateRelease({

View File

@ -3,19 +3,6 @@
<head lang="en">
<meta charset="UTF-8" />
<title>KeeWeb</title>
<meta name="application-name" content="KeeWeb" />
<meta name="kw-signature" content="" />
<meta name="kw-config" content="(no-config)" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="KeeWeb" />
<meta name="theme-color" content="#6386ec" />
<meta name="msapplication-config" content="browserconfig.xml" />
<meta name="msapplication-TileColor" content="#6386ec" />
<meta
http-equiv="Content-Security-Policy"
content="
@ -31,6 +18,19 @@
form-action 'none';
"
/>
<meta name="application-name" content="KeeWeb" />
<meta name="kw-signature" content="" />
<meta name="kw-config" content="(no-config)" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="KeeWeb" />
<meta name="theme-color" content="#6386ec" />
<meta name="msapplication-config" content="browserconfig.xml" />
<meta name="msapplication-TileColor" content="#6386ec" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />

View File

@ -41,7 +41,7 @@ DropboxChooser.prototype.buildUrl = function() {
};
DropboxChooser.prototype.onMessage = function(e) {
if (e.source !== this.popup) {
if (e.source !== this.popup || e.origin !== 'https://www.dropbox.com') {
return;
}
const data = JSON.parse(e.data);

View File

@ -21,7 +21,7 @@ const AuthReceiver = {
url.split(/[?#&]/g).forEach(part => {
const parts = part.split('=');
if (parts.length === 2) {
message[parts[0]] = parts[1];
message[parts[0]] = decodeURIComponent(parts[1]);
}
});
return message;

View File

@ -1,11 +1,10 @@
import { Events } from 'framework/events';
import { AuthReceiver } from 'comp/browser/auth-receiver';
import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts';
import { Links } from 'const/links';
import { Timeouts } from 'const/timeouts';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
const PopupNotifier = {
logger: null,
@ -14,7 +13,7 @@ const PopupNotifier = {
this.logger = new Logger('popup-notifier');
if (Launcher) {
window.open = this._openLauncherWindow.bind(this);
window.open = noop;
} else {
const windowOpen = window.open;
window.open = function(...args) {
@ -35,100 +34,6 @@ const PopupNotifier = {
}
},
_openLauncherWindow(url, title, settings) {
const opts = { show: false };
if (settings) {
const settingsObj = {};
settings.split(',').forEach(part => {
const parts = part.split('=');
settingsObj[parts[0].trim()] = parts[1].trim();
});
if (settingsObj.width) {
opts.width = +settingsObj.width;
}
if (settingsObj.height) {
opts.height = +settingsObj.height;
}
if (settingsObj.top) {
opts.y = +settingsObj.top;
}
if (settingsObj.left) {
opts.x = +settingsObj.left;
}
}
let win = Launcher.openWindow(opts);
win.webContents.on('will-redirect', (e, url) => {
if (PopupNotifier.isOwnUrl(url)) {
win.webContents.stop();
win.close();
PopupNotifier.processReturnToApp(url);
}
});
win.webContents.on('will-navigate', (e, url) => {
if (PopupNotifier.isOwnUrl(url)) {
e.preventDefault();
win.close();
PopupNotifier.processReturnToApp(url);
}
});
win.webContents.on('crashed', (e, killed) => {
this.logger.debug('crashed', e, killed);
this.deferCheckClosed(win);
win.close();
win = null;
});
win.webContents.on(
'did-fail-load',
(e, errorCode, errorDescription, validatedUrl, isMainFrame) => {
this.logger.debug(
'did-fail-load',
e,
errorCode,
errorDescription,
validatedUrl,
isMainFrame
);
this.deferCheckClosed(win);
win.close();
win = null;
}
);
win.once('page-title-updated', () => {
setTimeout(() => {
if (win) {
win.show();
win.focus();
}
}, Timeouts.PopupWaitTime);
});
win.on('closed', () => {
setTimeout(
PopupNotifier.triggerClosed.bind(PopupNotifier, win),
Timeouts.CheckWindowClosed
);
win = null;
});
win.loadURL(url);
Events.emit('popup-opened', win);
return win;
},
isOwnUrl(url) {
return (
url.lastIndexOf(Links.WebApp, 0) === 0 ||
url.lastIndexOf(location.origin + location.pathname, 0) === 0
);
},
processReturnToApp(url) {
const returnMessage = AuthReceiver.urlArgsToMessage(url);
if (Object.keys(returnMessage).length > 0) {
const evt = new Event('message');
evt.data = returnMessage;
window.dispatchEvent(evt);
}
},
deferCheckClosed(win) {
setTimeout(PopupNotifier.checkClosed.bind(PopupNotifier, win), Timeouts.CheckWindowClosed);
},

View File

@ -47,8 +47,8 @@ function walkEntry(db, entry, parents) {
let html = false;
if (field.markdown && AppSettingsModel.useMarkdown) {
const converted = MdToHtml.convert(value);
if (converted !== value) {
value = converted;
if (converted.html) {
value = converted.html;
html = true;
}
}

View File

@ -221,9 +221,6 @@ const Launcher = {
resolveProxy(url, callback) {
/* skip in cordova */
},
openWindow(opts) {
/* skip in cordova */
},
hideApp() {
/* skip in cordova */
},

View File

@ -205,9 +205,6 @@ const Launcher = {
callback(proxy);
});
},
openWindow(opts) {
return this.remoteApp().openWindow(opts);
},
hideApp() {
const app = this.remoteApp();
if (this.canMinimize()) {

View File

@ -0,0 +1,38 @@
// Secrets are not really secrets and are supposed to be embedded in the app code according to
// the Google's guide: https://developers.google.com/identity/protocols/oauth2#installed
// The process results in a client ID and, in some cases, a client secret,
// which you embed in the source code of your application.
// (In this context, the client secret is obviously not treated as a secret.)
const DropboxApps = {
AppFolder: { id: 'qp7ctun6qt5n9d6', secret: '07s5r4ck1uvlj6a' },
FullDropbox: { id: 'eor7hvv6u6oslq9', secret: 'ez04o1iwf6yprq3' }
};
const GDriveApps = {
Local: {
id: '783608538594-36tkdh8iscrq8t8dq87gghubnhivhjp5.apps.googleusercontent.com',
secret: 'yAtyfc9TIQ9GyQgQmo3i0HAP'
},
Production: {
id: '847548101761-koqkji474gp3i2gn3k5omipbfju7pbt1.apps.googleusercontent.com',
secret: '42HeSBybXDZjvweotq4o4CkJ'
},
Desktop: {
id: '847548101761-h2pcl2p6m1tssnlqm0vrm33crlveccbr.apps.googleusercontent.com',
secret: 'nTSCiqXtUNmURIIdASaC1TJK'
}
};
const OneDriveApps = {
Local: {
id: 'b97c53d5-db5b-4124-aab9-d39195293815',
secret: 'V9b6:iJU]N7cImE1f_OLNjqZJDBnumR?'
},
Production: {
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923',
secret: 'aOMJaktJEAs_Tmh]fx4iQ[Zd3mp3KK7-'
}
};
export { DropboxApps, GDriveApps, OneDriveApps };

View File

@ -0,0 +1,68 @@
const DefaultAppSettings = {
theme: 'fb', // UI theme
locale: null, // user interface language
expandGroups: true, // show entries from all subgroups
listViewWidth: null, // width of the entry list representation
menuViewWidth: null, // width of the left menu
tagsViewHeight: null, // tags menu section height
autoUpdate: 'install', // auto-update options: "install", "check", ""
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
autoSave: true, // auto-save open files
autoSaveInterval: 0, // interval between performing automatic sync, minutes
rememberKeyFiles: false, // remember keyfiles selected on the Open screen
idleMinutes: 15, // app lock timeout after inactivity, minutes
minimizeOnClose: false, // minimise the app instead of closing
tableView: false, // view entries as a table instead of list
colorfulIcons: false, // use colorful custom icons instead of grayscale
useMarkdown: true, // use Markdown in Notes field
directAutotype: true, // if only one matching entry is found, select that one automatically
titlebarStyle: 'default', // window titlebar style
lockOnMinimize: true, // lock the app when it's minimized
lockOnCopy: false, // lock the app after a password was copied
lockOnAutoType: false, // lock the app after performing auto-type
lockOnOsLock: false, // lock the app when the computer is locked
helpTipCopyShown: false, // disable the tooltip about copying fields
templateHelpShown: false, // disable the tooltip about entry templates
skipOpenLocalWarn: false, // disable the warning about opening a local file
hideEmptyFields: false, // hide empty fields in entries
skipHttpsWarning: false, // disable the non-HTTPS warning
demoOpened: false, // hide the demo button inside the More... menu
fontSize: 0, // font size: 0, 1, 2
tableViewColumns: null, // columns displayed in the table view
generatorPresets: null, // presets used in the password generator
generatorHidePassword: false, // hide password in the generator
cacheConfigSettings: false, // cache config settings and use them if the config can't be loaded
allowIframes: false, // allow displaying the app in IFrames
useGroupIconForEntries: false, // automatically use group icon when creating new entries
canOpen: true, // can select and open new files
canOpenDemo: true, // can open a demo file
canOpenSettings: true, // can go to settings
canCreate: true, // can create new files
canImportXml: true, // can import files from XML
canImportCsv: true, // can import files from CSV
canRemoveLatest: true, // can remove files from the recent file list
canExportXml: true, // can export files as XML
canExportHtml: true, // can export files as HTML
canSaveTo: true, // can save existing files to filesystem
canOpenStorage: true, // can open files from cloud storage providers
canOpenGenerator: true, // can open password generator
dropbox: true, // enable Dropbox integration
dropboxFolder: null, // default folder path
dropboxAppKey: null, // custom Dropbox app key
dropboxSecret: null, // custom Dropbox app secret
webdav: true, // enable WebDAV integration
webdavSaveMethod: 'move', // how to save files with WebDAV: "move" or "put"
gdrive: true, // enable Google Drive integration
gdriveClientId: null, // custom Google Drive client id
gdriveSecret: null, // custom Google Drive client secret
onedrive: true, // enable OneDrive integration
onedriveClientId: null, // custom OneDrive client id
onedriveSecret: null // custom OneDrive client secret
};
export { DefaultAppSettings };

View File

@ -444,6 +444,7 @@
"setGenLockAutoType": "On auto-type",
"setGenLockOrSleep": "When the computer is locked or put to sleep",
"setGenStorage": "Storage",
"setGenStorageLogout": "Log out",
"setGenShowAdvanced": "Show advanced settings",
"setGenDevTools": "Show dev tools",
"setGenTryBeta": "Try beta version until restart",
@ -607,6 +608,8 @@
"dropboxSetupDesc": "Some configuration is required to use Dropbox in a self-hosted app. Please create your own Dropbox app and fill in its key below.",
"dropboxAppKey": "Dropbox app key",
"dropboxAppKeyDesc": "Copy the key from your Dropbox app (Developer settings)",
"dropboxAppSecret": "Dropbox app secret",
"dropboxAppSecretDesc": "The secret can be found next to the app key",
"dropboxFolder": "App folder",
"dropboxFolderDesc": "If your app is linked to entire Dropbox (not app folder), set the folder with your kdbx files here",
"dropboxFolderSettingsDesc": "Select any folder in your Dropbox where files will be stored (root folder by default)",

View File

@ -1,5 +1,6 @@
import { Model } from 'framework/model';
import { SettingsStore } from 'comp/settings/settings-store';
import { DefaultAppSettings } from 'const/default-app-settings';
class AppSettingsModel extends Model {
constructor() {
@ -23,70 +24,17 @@ class AppSettingsModel extends Model {
}
save() {
SettingsStore.save('app-settings', this);
const values = {};
for (const [key, value] of Object.entries(this)) {
if (DefaultAppSettings[key] !== value) {
values[key] = value;
}
}
SettingsStore.save('app-settings', values);
}
}
AppSettingsModel.defineModelProperties(
{
theme: 'fb',
locale: null,
expandGroups: true,
listViewWidth: null,
menuViewWidth: null,
tagsViewHeight: null,
autoUpdate: 'install',
clipboardSeconds: 0,
autoSave: true,
autoSaveInterval: 0,
rememberKeyFiles: false,
idleMinutes: 15,
minimizeOnClose: false,
tableView: false,
colorfulIcons: false,
useMarkdown: true,
directAutotype: true,
titlebarStyle: 'default',
lockOnMinimize: true,
lockOnCopy: false,
lockOnAutoType: false,
lockOnOsLock: false,
helpTipCopyShown: false,
templateHelpShown: false,
skipOpenLocalWarn: false,
hideEmptyFields: false,
skipHttpsWarning: false,
demoOpened: false,
fontSize: 0,
tableViewColumns: null,
generatorPresets: null,
generatorHidePassword: false,
cacheConfigSettings: false,
allowIframes: false,
useGroupIconForEntries: false,
enableUsb: true,
canOpen: true,
canOpenDemo: true,
canOpenSettings: true,
canCreate: true,
canImportXml: true,
canImportCsv: true,
canRemoveLatest: true,
canExportXml: true,
canExportHtml: true,
canSaveTo: true,
canOpenWebdav: true,
canOpenGenerator: true,
canOpenOtpDevice: true,
dropbox: true,
webdav: true,
gdrive: true,
onedrive: true
},
{ extensions: true }
);
AppSettingsModel.defineModelProperties(DefaultAppSettings, { extensions: true });
const instance = new AppSettingsModel();

View File

@ -1,16 +1,14 @@
import { StorageBase } from 'storage/storage-base';
import { Features } from 'util/features';
import { UrlFormat } from 'util/formatting/url-format';
const DropboxKeys = {
AppFolder: 'qp7ctun6qt5n9d6',
FullDropbox: 'eor7hvv6u6oslq9'
};
import { DropboxApps } from 'const/cloud-storage-apps';
const DropboxCustomErrors = {
BadKey: 'bad-key'
};
// https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
class StorageDropbox extends StorageBase {
name = 'dropbox';
icon = 'dropbox';
@ -49,12 +47,23 @@ class StorageDropbox extends StorageBase {
}
_getKey() {
return this.appSettings.dropboxAppKey || DropboxKeys.AppFolder;
return this.appSettings.dropboxAppKey || DropboxApps.AppFolder.id;
}
_getSecret() {
const key = this._getKey();
if (key === DropboxApps.AppFolder.id) {
return DropboxApps.AppFolder.secret;
}
if (key === DropboxApps.FullDropbox.id) {
return DropboxApps.FullDropbox.secret;
}
return this.appSettings.dropboxSecret;
}
_isValidKey() {
const key = this._getKey();
const isBuiltIn = key === DropboxKeys.AppFolder || key === DropboxKeys.FullDropbox;
const isBuiltIn = key === DropboxApps.AppFolder.id || key === DropboxApps.FullDropbox.id;
return key && key.indexOf(' ') < 0 && (!isBuiltIn || this._canUseBuiltInKeys());
}
@ -66,14 +75,17 @@ class StorageDropbox extends StorageBase {
return {
scope: '',
url: 'https://www.dropbox.com/oauth2/authorize',
tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
clientId: this._getKey(),
clientSecret: this._getSecret(),
pkce: false,
width: 600,
height: 400
};
}
needShowOpenConfig() {
return !this._isValidKey();
return !this._isValidKey() || !this._getSecret();
}
getOpenConfig() {
@ -88,6 +100,14 @@ class StorageDropbox extends StorageBase {
required: true,
pattern: '\\w+'
},
{
id: 'secret',
title: 'dropboxAppSecret',
desc: 'dropboxAppSecretDesc',
type: 'text',
required: true,
pattern: '\\w+'
},
{
id: 'folder',
title: 'dropboxFolder',
@ -118,6 +138,15 @@ class StorageDropbox extends StorageBase {
pattern: '\\w+',
value: appKey
};
const secretField = {
id: 'secret',
title: 'dropboxAppSecret',
desc: 'dropboxAppSecretDesc',
type: 'text',
required: true,
pattern: '\\w+',
value: this.appSettings.dropboxSecret || ''
};
const folderField = {
id: 'folder',
title: 'dropboxFolder',
@ -128,24 +157,26 @@ class StorageDropbox extends StorageBase {
const canUseBuiltInKeys = this._canUseBuiltInKeys();
if (canUseBuiltInKeys) {
fields.push(linkField);
if (appKey === DropboxKeys.AppFolder) {
if (appKey === DropboxApps.AppFolder.id) {
linkField.value = 'app';
} else if (appKey === DropboxKeys.FullDropbox) {
} else if (appKey === DropboxApps.FullDropbox.id) {
linkField.value = 'full';
fields.push(folderField);
} else {
fields.push(keyField);
fields.push(secretField);
fields.push(folderField);
}
} else {
fields.push(keyField);
fields.push(secretField);
fields.push(folderField);
}
return { fields };
}
applyConfig(config, callback) {
if (config.key === DropboxKeys.AppFolder || config.key === DropboxKeys.FullDropbox) {
if (config.key === DropboxApps.AppFolder.id || config.key === DropboxApps.FullDropbox.id) {
return callback(DropboxCustomErrors.BadKey);
}
// TODO: try to connect using new key
@ -154,6 +185,7 @@ class StorageDropbox extends StorageBase {
}
this.appSettings.set({
dropboxAppKey: config.key,
dropboxSecret: config.secret,
dropboxFolder: config.folder
});
callback();
@ -165,10 +197,10 @@ class StorageDropbox extends StorageBase {
key = 'dropboxAppKey';
switch (value) {
case 'app':
value = DropboxKeys.AppFolder;
value = DropboxApps.AppFolder.id;
break;
case 'full':
value = DropboxKeys.FullDropbox;
value = DropboxApps.FullDropbox.id;
break;
case 'custom':
value = '(your app key)';
@ -176,11 +208,15 @@ class StorageDropbox extends StorageBase {
default:
return;
}
this._oauthRevokeToken();
this.logout();
break;
case 'key':
key = 'dropboxAppKey';
this._oauthRevokeToken();
this.logout();
break;
case 'secret':
key = 'dropboxSecret';
this.logout();
break;
case 'folder':
key = 'dropboxFolder';
@ -365,11 +401,10 @@ class StorageDropbox extends StorageBase {
});
}
setEnabled(enabled) {
if (!enabled) {
this._oauthRevokeToken();
}
super.setEnabled(enabled);
logout() {
this._oauthRevokeToken('https://api.dropboxapi.com/2/auth/token/revoke', {
method: 'POST'
});
}
}

View File

@ -1,22 +1,12 @@
import { StorageBase } from 'storage/storage-base';
import { Locale } from 'util/locale';
import { Features } from 'util/features';
import { GDriveApps } from 'const/cloud-storage-apps';
const GDriveClientId = {
Local: '783608538594-36tkdh8iscrq8t8dq87gghubnhivhjp5.apps.googleusercontent.com',
Production: '847548101761-koqkji474gp3i2gn3k5omipbfju7pbt1.apps.googleusercontent.com',
Desktop: '847548101761-h2pcl2p6m1tssnlqm0vrm33crlveccbr.apps.googleusercontent.com'
};
const GDriveClientSecret = {
// They are not really secrets and are supposed to be embedded in the app code according to
// the official guide: https://developers.google.com/identity/protocols/oauth2#installed
// The process results in a client ID and, in some cases, a client secret,
// which you embed in the source code of your application.
// (In this context, the client secret is obviously not treated as a secret.)
Desktop: 'nTSCiqXtUNmURIIdASaC1TJK'
};
const NewFileIdPrefix = 'NewFile:';
// https://developers.google.com/identity/protocols/oauth2/web-server
class StorageGDrive extends StorageBase {
name = 'gdrive';
enabled = true;
@ -239,25 +229,20 @@ class StorageGDrive extends StorageBase {
});
}
setEnabled(enabled) {
if (!enabled) {
this._oauthRevokeToken('https://accounts.google.com/o/oauth2/revoke?token={token}');
}
super.setEnabled(enabled);
logout() {
this._oauthRevokeToken('https://accounts.google.com/o/oauth2/revoke?token={token}');
}
_getOAuthConfig() {
let clientId = this.appSettings.gdriveClientId;
let clientSecret;
if (!clientId) {
let clientSecret = this.appSettings.gdriveClientSecret;
if (!clientId || !clientSecret) {
if (Features.isDesktop) {
clientId = GDriveClientId.Desktop;
clientSecret = GDriveClientSecret.Desktop;
({ id: clientId, secret: clientSecret } = GDriveApps.Desktop);
} else if (Features.isLocal) {
({ id: clientId, secret: clientSecret } = GDriveApps.Local);
} else {
clientId =
location.origin.indexOf('localhost') >= 0
? GDriveClientId.Local
: GDriveClientId.Production;
({ id: clientId, secret: clientSecret } = GDriveApps.Production);
}
}
return {
@ -267,13 +252,13 @@ class StorageGDrive extends StorageBase {
clientId,
clientSecret,
width: 600,
height: 400
height: 400,
pkce: true,
redirectUrlParams: {
'access_type': 'offline'
}
};
}
_useLocalOAuthRedirectListener() {
return Features.isDesktop;
}
}
export { StorageGDrive };

View File

@ -1,10 +1,8 @@
import { StorageBase } from 'storage/storage-base';
import { noop } from 'util/fn';
import { OneDriveApps } from 'const/cloud-storage-apps';
import { Features } from 'util/features';
const OneDriveClientId = {
Production: '000000004818ED3A',
Local: '0000000044183D18'
};
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
class StorageOneDrive extends StorageBase {
name = 'onedrive';
@ -216,56 +214,31 @@ class StorageOneDrive extends StorageBase {
});
}
setEnabled(enabled) {
if (!enabled) {
const url = 'https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url}'.replace(
'{url}',
this._getOauthRedirectUrl()
);
this._oauthRevokeToken(url);
}
super.setEnabled(enabled);
}
_getClientId() {
let clientId = this.appSettings.onedriveClientId;
if (!clientId) {
clientId =
location.origin.indexOf('localhost') >= 0
? OneDriveClientId.Local
: OneDriveClientId.Production;
}
return clientId;
logout(enabled) {
this._oauthRevokeToken();
}
_getOAuthConfig() {
const clientId = this._getClientId();
let clientId = this.appSettings.onedriveClientId;
let clientSecret = this.appSettings.onedriveClientSecret;
if (!clientId || !clientSecret) {
if (Features.isLocal) {
({ id: clientId, secret: clientSecret } = OneDriveApps.Local);
} else {
({ id: clientId, secret: clientSecret } = OneDriveApps.Production);
}
}
return {
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
scope: 'files.readwrite',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
scope: 'files.readwrite offline_access',
clientId,
clientSecret,
pkce: true,
width: 600,
height: 500
};
}
_popupOpened(popupWindow) {
if (popupWindow.webContents) {
popupWindow.webContents.on('did-finish-load', e => {
const webContents = e.sender.webContents;
const url = webContents.getURL();
if (
url &&
url.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')
) {
// click the login button mentioned in #821
const script = `const selector = '[role="button"][aria-describedby="tileError loginHeader"]';
if (document.querySelectorAll(selector).length === 1) document.querySelector(selector).click()`;
webContents.executeJavaScript(script).catch(noop);
}
});
}
}
}
export { StorageOneDrive };

View File

@ -6,6 +6,7 @@ import { StorageFileCache } from 'storage/impl/storage-file-cache';
import { StorageGDrive } from 'storage/impl/storage-gdrive';
import { StorageOneDrive } from 'storage/impl/storage-onedrive';
import { StorageWebDav } from 'storage/impl/storage-webdav';
import { createOAuthSession } from 'storage/pkce';
const BuiltInStorage = {
file: new StorageFile(),
@ -24,4 +25,6 @@ if (!Launcher || Launcher.thirdPartyStoragesSupported) {
Object.assign(Storage, ThirdPartyStorage);
}
requestAnimationFrame(createOAuthSession);
export { Storage };

View File

@ -0,0 +1,32 @@
import kdbxweb from 'kdbxweb';
let newOAuthSession;
function createOAuthSession() {
const session = newOAuthSession;
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(64));
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(50));
const codeVerifierBytes = kdbxweb.ByteUtils.arrayToBuffer(
kdbxweb.ByteUtils.stringToBytes(codeVerifier)
);
kdbxweb.CryptoEngine.sha256(codeVerifierBytes).then(hash => {
const codeChallenge = kdbxweb.ByteUtils.bytesToBase64(hash)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
newOAuthSession = {
state,
codeChallenge,
codeVerifier
};
});
newOAuthSession = null;
return session;
}
export { createOAuthSession };

View File

@ -8,6 +8,8 @@ import { UrlFormat } from 'util/formatting/url-format';
import { Launcher } from 'comp/launcher';
import { omitEmpty } from 'util/fn';
import { Timeouts } from 'const/timeouts';
import { Features } from 'util/features';
import { createOAuthSession } from 'storage/pkce';
const MaxRequestRetries = 3;
@ -38,9 +40,18 @@ class StorageBase {
}
setEnabled(enabled) {
if (!enabled) {
this.logout();
}
this.enabled = enabled;
}
get loggedIn() {
return !!this.runtimeData[this.name + 'OAuthToken'];
}
logout() {}
_xhr(config) {
this.logger.info('HTTP request', config.method || 'GET', config.url);
if (config.data) {
@ -88,8 +99,8 @@ class StorageBase {
}
_httpRequest(config, onLoad) {
const httpRequest = Launcher ? this._httpRequestLauncher : this._httpRequestWeb;
httpRequest(config, onLoad);
const httpRequest = Features.isDesktop ? this._httpRequestLauncher : this._httpRequestWeb;
httpRequest.call(this, config, onLoad);
}
_httpRequestWeb(config, onLoad) {
@ -127,49 +138,81 @@ class StorageBase {
}
_httpRequestLauncher(config, onLoad) {
const https = Launcher.req('https');
const req = https.request(config.url, {
method: config.method || 'GET',
headers: config.headers,
timeout: Timeouts.DefaultHttpRequest
});
req.on('response', res => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
let response = Buffer.concat(chunks);
if (config.responseType === 'json') {
response = JSON.parse(response.toString('utf8'));
Launcher.resolveProxy(config.url, proxy => {
const https = Launcher.req('https');
const opts = Launcher.req('url').parse(config.url);
opts.method = config.method || 'GET';
opts.headers = {
'User-Agent': navigator.userAgent,
...config.headers
};
opts.timeout = Timeouts.DefaultHttpRequest;
let data;
if (config.data) {
if (config.dataIsMultipart) {
data = Buffer.concat(config.data.map(chunk => Buffer.from(chunk)));
} else {
response = response.buffer.slice(
response.byteOffset,
response.byteOffset + response.length
);
data = Buffer.from(config.data);
}
onLoad({
status: res.statusCode,
response,
getResponseHeader: name => res.headers[name.toLowerCase()]
opts.headers['Content-Length'] = data.byteLength;
}
if (proxy) {
opts.headers.Host = opts.host;
opts.host = proxy.host;
opts.port = proxy.port;
opts.path = config.url;
}
const req = https.request(opts);
req.on('response', res => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
this.logger.debug(
'HTTP response',
opts.method,
config.url,
res.statusCode,
res.headers
);
let response = Buffer.concat(chunks);
if (config.responseType === 'json') {
try {
response = JSON.parse(response.toString('utf8'));
} catch (e) {
return config.error && config.error('json parse error');
}
} else {
response = response.buffer.slice(
response.byteOffset,
response.byteOffset + response.length
);
}
onLoad({
status: res.statusCode,
response,
getResponseHeader: name => res.headers[name.toLowerCase()]
});
});
});
});
req.on('error', () => {
return config.error && config.error('network error', {});
});
req.on('timeout', () => {
req.abort();
return config.error && config.error('timeout', {});
});
if (config.data) {
let data;
if (config.dataIsMultipart) {
data = Buffer.concat(config.data.map(chunk => Buffer.from(chunk)));
} else {
data = Buffer.from(config.data);
req.on('error', () => {
return config.error && config.error('network error', {});
});
req.on('timeout', () => {
req.abort();
return config.error && config.error('timeout', {});
});
if (data) {
req.write(data);
}
req.write(data);
}
req.end();
req.end();
});
}
_openPopup(url, title, width, height, extras) {
@ -231,58 +274,67 @@ class StorageBase {
return this._oauthExchangeRefreshToken(callback);
}
if (this._useLocalOAuthRedirectListener()) {
return StorageOAuthListener.listen()
.then(listener => {
const url = UrlFormat.makeUrl(opts.url, {
'client_id': opts.clientId,
'scope': opts.scope,
'state': listener.state,
'redirect_uri': listener.redirectUri,
'response_type': 'code',
'code_challenge': listener.codeChallenge,
'code_challenge_method': 'S256'
});
Launcher.openLink(url);
callback('browser-auth-started');
listener.callback = code => this._oauthCodeReceived(code, listener);
})
.catch(err => callback(err));
const session = createOAuthSession();
let listener;
if (Features.isDesktop) {
listener = StorageOAuthListener.listen();
session.redirectUri = listener.redirectUri;
} else {
session.redirectUri = this._getOauthRedirectUrl();
}
const pkceParams = opts.pkce
? {
'code_challenge': session.codeChallenge,
'code_challenge_method': 'S256'
}
: undefined;
const url = UrlFormat.makeUrl(opts.url, {
'client_id': opts.clientId,
'scope': opts.scope,
'response_type': 'token',
'redirect_uri': this._getOauthRedirectUrl()
'state': session.state,
'redirect_uri': session.redirectUri,
'response_type': 'code',
...pkceParams
});
this.logger.debug('OAuth: popup opened');
if (listener) {
listener.on('ready', () => {
Launcher.openLink(url);
callback('browser-auth-started');
});
listener.on('error', err => callback(err));
listener.on('result', result => this._oauthCodeReceived(result, session));
return;
}
const popupWindow = this._openPopup(url, 'OAuth', opts.width, opts.height);
if (!popupWindow) {
return callback('OAuth: cannot open popup');
}
this._popupOpened(popupWindow);
this.logger.debug('OAuth: popup opened');
const popupClosed = () => {
Events.off('popup-closed', popupClosed);
window.removeEventListener('message', windowMessage);
this.logger.error('OAuth error', 'popup closed');
callback('OAuth: popup closed');
};
const windowMessage = e => {
if (!e.data) {
if (e.origin !== location.origin) {
return;
}
const token = this._oauthProcessReturn(e.data);
if (token) {
if (e.data && e.data.error) {
this.logger.error('OAuth error', e.data.error, e.data.error_description);
callback('OAuth: ' + e.data.error);
} else if (e.data && e.data.code) {
Events.off('popup-closed', popupClosed);
window.removeEventListener('message', windowMessage);
if (token.error) {
this.logger.error('OAuth error', token.error, token.errorDescription);
callback('OAuth: ' + token.error);
} else {
callback();
}
this._oauthCodeReceived(e.data, session, callback);
} else {
this.logger.debug('Skipped OAuth message', e.data);
}
@ -291,8 +343,6 @@ class StorageBase {
window.addEventListener('message', windowMessage);
}
_popupOpened(popupWindow) {}
_oauthProcessReturn(message) {
const token = this._oauthMsgToToken(message);
if (token && !token.error) {
@ -333,13 +383,14 @@ class StorageBase {
}
}
_oauthRevokeToken(url) {
_oauthRevokeToken(url, requestOptions) {
const token = this.runtimeData[this.name + 'OAuthToken'];
if (token) {
if (url) {
this._xhr({
url: url.replace('{token}', token.accessToken),
statuses: [200, 401]
statuses: [200, 401],
...requestOptions
});
}
delete this.runtimeData[this.name + 'OAuthToken'];
@ -357,34 +408,54 @@ class StorageBase {
return true;
}
_useLocalOAuthRedirectListener() {
return false;
}
_oauthCodeReceived(result, session, callback) {
if (!result.state) {
this.logger.info('OAuth result has no state');
return callback && callback('OAuth result has no state');
}
if (result.state !== session.state) {
this.logger.info('OAuth result has bad state');
return callback && callback('OAuth result has bad state');
}
if (!result.code) {
this.logger.info('OAuth result has no code');
return callback && callback('OAuth result has no code');
}
_oauthCodeReceived(code, listener) {
this.logger.debug('OAuth code received');
Launcher.showMainWindow();
if (Features.isDesktop) {
Launcher.showMainWindow();
}
const config = this._getOAuthConfig();
const pkceParams = config.pkce ? { 'code_verifier': session.codeVerifier } : undefined;
this._xhr({
url: config.tokenUrl,
method: 'POST',
responseType: 'json',
skipAuth: true,
data: JSON.stringify({
data: UrlFormat.buildFormData({
'client_id': config.clientId,
'client_secret': config.clientSecret,
'grant_type': 'authorization_code',
code,
'code_verifier': listener.codeVerifier,
'redirect_uri': listener.redirectUri
'code': result.code,
'redirect_uri': session.redirectUri,
...pkceParams
}),
dataType: 'application/json',
dataType: 'application/x-www-form-urlencoded',
success: response => {
this.logger.debug('OAuth code exchanged');
this._oauthProcessReturn(response);
this.logger.debug('OAuth code exchanged', response);
const token = this._oauthProcessReturn(response);
if (token && token.error) {
return callback && callback('OAuth code exchange error: ' + token.error);
}
callback && callback();
},
error: err => {
this.logger.error('Error exchanging OAuth code', err);
callback && callback('OAuth code exchange error: ' + err);
}
});
}
@ -398,13 +469,13 @@ class StorageBase {
method: 'POST',
responseType: 'json',
skipAuth: true,
data: JSON.stringify({
data: UrlFormat.buildFormData({
'client_id': config.clientId,
'client_secret': config.clientSecret,
'grant_type': 'refresh_token',
'refresh_token': refreshToken
}),
dataType: 'application/json',
dataType: 'application/x-www-form-urlencoded',
success: response => {
this.logger.debug('Refresh token exchanged');
this._oauthProcessReturn({

View File

@ -1,6 +1,6 @@
import EventEmitter from 'events';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
import { noop } from 'util/fn';
import { Locale } from 'util/locale';
const DefaultPort = 48149;
@ -10,40 +10,41 @@ const StorageOAuthListener = {
server: null,
listen() {
return new Promise((resolve, reject) => {
if (this.server) {
this.stop();
}
if (this.server) {
this.stop();
}
const listener = {
callback: noop,
state: Math.round(Math.random() * Date.now()).toString()
};
const http = Launcher.req('http');
const server = http.createServer((req, resp) => {
resp.writeHead(200, 'OK', {
'Content-Type': 'text/plain; charset=UTF-8'
});
resp.end(Locale.appBrowserAuthComplete);
this.handleResult(req.url, listener);
});
const port = DefaultPort;
logger.info(`Starting OAuth listener on port ${port}...`);
server.listen(port);
server.on('error', err => {
logger.error('Failed to start OAuth listener', err);
reject('Failed to start OAuth listener: ' + err);
server.close();
});
server.on('listening', () => {
this.server = server;
listener.redirectUri = `http://127.0.0.1:${port}/oauth-result`;
this._setCodeVerifier(listener);
resolve(listener);
});
const listener = {};
Object.keys(EventEmitter.prototype).forEach(key => {
listener[key] = EventEmitter.prototype[key];
});
const http = Launcher.req('http');
const server = http.createServer((req, resp) => {
resp.writeHead(200, 'OK', {
'Content-Type': 'text/plain; charset=UTF-8'
});
resp.end(Locale.appBrowserAuthComplete);
this.handleResult(req.url, listener);
});
const port = DefaultPort;
logger.info(`Starting OAuth listener on port ${port}...`);
server.listen(port);
server.on('error', err => {
logger.error('Failed to start OAuth listener', err);
listener.emit('error', 'Failed to start OAuth listener: ' + err);
server.close();
});
server.on('listening', () => {
this.server = server;
listener.emit('ready');
});
listener.redirectUri = `http://localhost:${port}/oauth-result`;
return listener;
},
stop() {
@ -58,35 +59,8 @@ const StorageOAuthListener = {
this.stop();
url = new URL(url, 'http://localhost');
const state = url.searchParams.get('state');
if (!state) {
logger.info('OAuth result has no state');
return;
}
if (state !== listener.state) {
logger.info('OAuth result has bad state');
return;
}
const code = url.searchParams.get('code');
if (!code) {
logger.info('OAuth result has no code');
return;
}
listener.callback(code);
},
_setCodeVerifier(listener) {
const crypto = Launcher.req('crypto');
listener.codeVerifier = crypto.randomBytes(50).toString('hex');
const hash = crypto.createHash('sha256');
hash.update(listener.codeVerifier);
listener.codeChallenge = hash
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
listener.emit('result', { state, code });
}
};

View File

@ -15,6 +15,7 @@ const Features = {
isSelfHosted:
!isDesktop &&
!/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href),
isLocal: location.origin.indexOf('localhost') >= 0,
needFixClicks: /Edge\/14/.test(navigator.appVersion),
canUseWasmInWebWorker: !isDesktop && !/Chrome/.test(navigator.appVersion),

View File

@ -21,10 +21,10 @@ const MdToHtml = {
const htmlWithoutLineBreaks = html.replace(whiteSpaceRegex, '');
const mdWithoutLineBreaks = md.replace(whiteSpaceRegex, '');
if (htmlWithoutLineBreaks === mdWithoutLineBreaks) {
return md;
return { text: md };
} else {
const sanitized = dompurify.sanitize(html, { ADD_ATTR: ['target'] });
return `<div class="markdown">${sanitized}</div>`;
return { html: `<div class="markdown">${sanitized}</div>` };
}
}
};

View File

@ -29,6 +29,12 @@ const UrlFormat = {
.map(([key, value]) => key + '=' + encodeURIComponent(value))
.join('&');
return base + '?' + queryString;
},
buildFormData(params) {
return Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
}
};

View File

@ -44,7 +44,7 @@ Tip.prototype.show = function() {
const tipEl = (this.tipEl = $('<div></div>')
.addClass('tip')
.appendTo('body')
.html(this.title));
.text(this.title));
const rect = this.el[0].getBoundingClientRect();
const tipRect = this.tipEl[0].getBoundingClientRect();
const placement = this.placement || this.getAutoPlacement(rect, tipRect);

View File

@ -24,7 +24,11 @@ class FieldViewText extends FieldView {
if (value && value.isProtected) {
value = value.getText();
}
return MdToHtml.convert(value);
const converted = MdToHtml.convert(value);
if (converted.html) {
return converted.html;
}
value = converted.text;
}
return value && value.isProtected
? PasswordPresenter.presentValueWithLineBreaks(value)

View File

@ -1,22 +1,35 @@
import { FieldViewText } from 'views/fields/field-view-text';
import { escape } from 'util/fn';
const AllowedProtocols = ['http:', 'https:', 'ftp:', 'ftps:', 'mailto:'];
class FieldViewUrl extends FieldViewText {
displayUrlRegex = /^https:\/\//i;
cssClass = 'url';
renderValue(value) {
return value
? '<a href="' +
escape(this.fixUrl(value)) +
'" rel="noreferrer noopener" target="_blank">' +
escape(this.displayUrl(value)) +
'</a>'
: '';
try {
return value
? '<a href="' +
escape(this.fixUrl(value)) +
'" rel="noreferrer noopener" target="_blank">' +
escape(this.displayUrl(value)) +
'</a>'
: '';
} catch (e) {
return escape(value);
}
}
fixUrl(url) {
return url.indexOf(':') < 0 ? 'https://' + url : url;
const proto = new URL(url, 'dummy://').protocol;
if (proto === 'dummy:') {
return 'https://' + url;
}
if (!AllowedProtocols.includes(proto)) {
throw new Error('Bad url');
}
return url;
}
displayUrl(url) {

View File

@ -135,7 +135,7 @@ class ListSearchView extends View {
setLocale() {
this.sortOptions.forEach(opt => {
opt.text = opt.loc();
opt.html = opt.loc();
});
const entryDesc = Features.isMobile
? ''
@ -145,7 +145,7 @@ class ListSearchView extends View {
Shortcuts.altShortcutSymbol(true) +
'N)</span>';
this.createOptions = [
{ value: 'entry', icon: 'key', text: StringFormat.capFirst(Locale.entry) + entryDesc },
{ value: 'entry', icon: 'key', html: StringFormat.capFirst(Locale.entry) + entryDesc },
{ value: 'group', icon: 'folder', text: StringFormat.capFirst(Locale.group) }
];
if (this.el) {

View File

@ -83,15 +83,14 @@ class OpenView extends View {
clearTimeout(this.dragTimeout);
}
const storageProviders = [];
Object.keys(Storage).forEach(name => {
const prv = Storage[name];
if (!prv.system && prv.enabled) {
if (name === 'webdav' && !this.model.settings.canOpenWebdav) {
return;
if (this.model.settings.canOpenStorage) {
Object.keys(Storage).forEach(name => {
const prv = Storage[name];
if (!prv.system && prv.enabled) {
storageProviders.push(prv);
}
storageProviders.push(prv);
}
});
});
}
storageProviders.sort((x, y) => (x.uipos || Infinity) - (y.uipos || Infinity));
const showMore =
storageProviders.length ||

View File

@ -49,6 +49,7 @@ class SettingsGeneralView extends View {
'click .settings__general-download-update-btn': 'downloadUpdate',
'click .settings__general-update-found-btn': 'installFoundUpdate',
'change .settings__general-prv-check': 'changeStorageEnabled',
'click .settings__general-prv-logout': 'logoutFromStorage',
'click .settings__general-show-advanced': 'showAdvancedSettings',
'click .settings__general-dev-tools-link': 'openDevTools',
'click .settings__general-try-beta-link': 'tryBeta',
@ -195,7 +196,8 @@ class SettingsGeneralView extends View {
return storageProviders.map(sp => ({
name: sp.name,
enabled: sp.enabled,
hasConfig: !!sp.getSettingsConfig
hasConfig: !!sp.getSettingsConfig,
loggedIn: sp.loggedIn
}));
}
@ -354,6 +356,14 @@ class SettingsGeneralView extends View {
}
}
logoutFromStorage(e) {
const storage = Storage[$(e.target).data('storage')];
if (storage) {
storage.logout();
$(e.target).remove();
}
}
showAdvancedSettings() {
this.$el
.find('.settings__general-show-advanced, .settings__general-advanced')

View File

@ -171,6 +171,9 @@
&__general-prv {
margin-bottom: $base-padding-v;
}
&__general-prv-logout {
margin-bottom: $base-padding-v;
}
&__logs {
user-select: text;
margin-top: $base-padding-v;

View File

@ -2,7 +2,13 @@
{{#each options as |option|}}
<div class="dropdown__item {{#if option.active}}dropdown__item--active{{/if}}" data-value="{{option.value}}">
<i class="fa fa-{{option.icon}} dropdown__item-icon"></i>
<span class="dropdown__item-text">{{{option.text}}}</span>
<span class="dropdown__item-text">
{{~#if option.text~}}
{{option.text}}
{{~else~}}
{{{option.html}}}
{{~/if~}}
</span>
</div>
{{/each}}
</div>

View File

@ -205,6 +205,8 @@
data-storage="{{prv.name}}" {{#if prv.enabled}}checked{{/if}}
/><label for="settings__general-prv-check-{{prv.name}}">{{res prv.name}}</label></h4>
<div class="settings__general-prv-wrap settings__general-{{prv.name}} {{#ifeq prv.enabled false}}hide{{/ifeq}}"></div>
{{#if prv.loggedIn}}<button class="btn-silent settings__general-prv-logout"
data-storage="{{prv.name}}">{{res 'setGenStorageLogout'}}</button>{{/if}}
{{/each}}
<h2>{{res 'advanced'}}</h2>

View File

@ -98,6 +98,18 @@ app.on('second-instance', () => {
restoreMainWindow();
}
});
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', async (e, url) => {
e.preventDefault();
emitRemoteEvent('log', { message: `Prevented new window: ${url}` });
});
contents.on('will-navigate', (e, url) => {
if (!url.startsWith('https://beta.keeweb.info/') && !url.startsWith(htmlPath)) {
e.preventDefault();
emitRemoteEvent('log', { message: `Prevented navigation: ${url}` });
}
});
});
app.restartApp = function() {
restartPending = true;
mainWindow.close();
@ -105,9 +117,6 @@ app.restartApp = function() {
restartPending = false;
}, 1000);
};
app.openWindow = function(opts) {
return new electron.BrowserWindow(opts);
};
app.minimizeApp = function(menuItemLabels) {
let imagePath;
mainWindow.hide();
@ -230,12 +239,6 @@ function createMainWindow() {
mainWindow.on('session-end', () => {
emitRemoteEvent('os-lock');
});
mainWindow.webContents.on('will-navigate', (e, url) => {
if (!url.startsWith('https://beta.keeweb.info/') && !url.startsWith(htmlPath)) {
emitRemoteEvent('log', { message: `Prevented navigation: ${url}` });
e.preventDefault();
}
});
perfTimestamps &&
perfTimestamps.push({ name: 'configuring main window', ts: process.hrtime() });

View File

@ -1,6 +1,6 @@
{
"name": "KeeWeb",
"version": "1.13.4",
"version": "1.14.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "KeeWeb",
"version": "1.13.4",
"version": "1.14.0",
"description": "Free cross-platform password manager compatible with KeePass",
"main": "main.js",
"homepage": "https://keeweb.info",

342
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "keeweb",
"version": "1.13.4",
"version": "1.14.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1351,12 +1351,25 @@
}
},
"@develar/schema-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.1.0.tgz",
"integrity": "sha512-qjCqB4ctMig9Gz5bd6lkdFr3bO6arOdQqptdBSpF1ZpCnjofieCciEzkoS9ujY9cMGyllYSCSmBJ3x9OKHXzoA==",
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
"integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
"requires": {
"ajv": "^6.1.0",
"ajv-keywords": "^3.1.0"
"ajv": "^6.12.0",
"ajv-keywords": "^3.4.1"
},
"dependencies": {
"ajv": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
}
}
},
"@electron/get": {
@ -1807,54 +1820,39 @@
}
},
"app-builder-bin": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.5.5.tgz",
"integrity": "sha512-ZcHzJ9Xl+azPqdKzXZKdRZmkNmbxHHZyl4cbobNf8qMQpoPChpcov8riVrZSbu/0cT/JqJ8LOwJjy1OAwbChaQ=="
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.5.6.tgz",
"integrity": "sha512-gY9ABoV5jh67IrPEwF81R8l9LwE3RqHUyU3rIKitwqMpKhplN5OZC6WEHOXO3XhwiLCIlr9LLI6OPhr3bmtQIg=="
},
"app-builder-lib": {
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-22.4.1.tgz",
"integrity": "sha512-epwUzIM+2pcdy/If9koTP74CKx4v7xGPj75a2Z5cM4rrGN9yVZ3eDUBbfF0e0qE4Qmcv5pd0BAZJ26bGm8NWsQ==",
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-22.5.1.tgz",
"integrity": "sha512-VtB/PD8actR1317D/0uGzuJIYbpw4pRrfMB6IyTLwGynUd3ihqiCFjejVWHjCwopgCct2kE0MvLwo8P49xHIeQ==",
"requires": {
"7zip-bin": "~5.0.3",
"@develar/schema-utils": "~2.1.0",
"@develar/schema-utils": "~2.6.5",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
"builder-util": "22.4.1",
"builder-util-runtime": "8.6.2",
"builder-util": "22.5.1",
"builder-util-runtime": "8.7.0",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.1.1",
"ejs": "^3.0.1",
"electron-publish": "22.4.1",
"fs-extra": "^8.1.0",
"ejs": "^3.0.2",
"electron-publish": "22.5.1",
"fs-extra": "^9.0.0",
"hosted-git-info": "^3.0.4",
"is-ci": "^2.0.0",
"isbinaryfile": "^4.0.4",
"isbinaryfile": "^4.0.5",
"js-yaml": "^3.13.1",
"lazy-val": "^1.0.4",
"minimatch": "^3.0.4",
"normalize-package-data": "^2.5.0",
"read-config-file": "5.0.2",
"read-config-file": "6.0.0",
"sanitize-filename": "^1.6.3",
"semver": "^7.1.3",
"temp-file": "^3.3.7"
},
"dependencies": {
"ejs": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.0.1.tgz",
"integrity": "sha512-cuIMtJwxvzumSAkqaaoGY/L6Fc/t6YvoP9/VIaK0V/CyqKLEQ8sqODmYfy/cjXEdZ9+OOL8TecbJu+1RsofGDw=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"hosted-git-info": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz",
@ -1864,9 +1862,9 @@
}
},
"isbinaryfile": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.5.tgz",
"integrity": "sha512-Jvz0gpTh1AILHMCBUyqq7xv1ZOQrxTDwyp1/QUq1xFpOBvp4AH5uEobPePJht8KnBGqQIH7We6OR73mXsjG0cA=="
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz",
"integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg=="
},
"lru-cache": {
"version": "5.1.1",
@ -1877,9 +1875,9 @@
}
},
"semver": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA=="
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
},
"yallist": {
"version": "3.1.1",
@ -3224,19 +3222,19 @@
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
},
"builder-util": {
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-22.4.1.tgz",
"integrity": "sha512-+ysLc7cC4w6P7rBxmZ5X2aU3QvcwFoWCl1us+mcUKdsGmJAtFUMPJqueeptdxjyPrPShIUOKHzA8uk5A3d1fHg==",
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-22.5.1.tgz",
"integrity": "sha512-CelDTP3+fvDfZfbwy3PXif7mudPaWankJ8vrRg/NtCGvL+hXnwycnJZr46d5EQL7AgQcpJ27o9LTdfu61cxTFw==",
"requires": {
"7zip-bin": "~5.0.3",
"@types/debug": "^4.1.5",
"@types/fs-extra": "^8.1.0",
"app-builder-bin": "3.5.5",
"app-builder-bin": "3.5.6",
"bluebird-lst": "^1.0.9",
"builder-util-runtime": "8.6.2",
"builder-util-runtime": "8.7.0",
"chalk": "^3.0.0",
"debug": "^4.1.1",
"fs-extra": "^8.1.0",
"fs-extra": "^9.0.0",
"is-ci": "^2.0.0",
"js-yaml": "^3.13.1",
"source-map-support": "^0.5.16",
@ -3275,16 +3273,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -3315,9 +3303,9 @@
}
},
"builder-util-runtime": {
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.6.2.tgz",
"integrity": "sha512-9QnIBISfhgQ2BxtRLidVqf/v5HD73vSKZDllpUmGd2L6VORGQk7cZAPmPtw4HQM3gPBelyVJ5yIjMNZ8xjmd1A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.0.tgz",
"integrity": "sha512-G1AqqVM2vYTrSFR982c1NNzwXKrGLQjVjaZaWQdn4O6Z3YKjdMDofw88aD9jpyK9ZXkrCxR0tI3Qe9wNbyTlXg==",
"requires": {
"debug": "^4.1.1",
"sax": "^1.2.4"
@ -4759,29 +4747,18 @@
}
},
"dmg-builder": {
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-22.4.1.tgz",
"integrity": "sha512-hEemh7n0zoVt7zPPwvn7iOttP03oENjJ4ApttPmt8oDnX8T4q42MjGWyDlLkPMplMJfoTxkkNqmm296f0OYM8Q==",
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-22.5.1.tgz",
"integrity": "sha512-AwIiyGwgqhA8Ty/YnEU20aSzfrWZns6suOBTqddD+rLDI4jEASKGQadfvcXRSWgaK/VQW0GrhheXrhJpzZzt3g==",
"requires": {
"app-builder-lib": "~22.4.1",
"bluebird-lst": "^1.0.9",
"builder-util": "~22.4.1",
"fs-extra": "^8.1.0",
"app-builder-lib": "22.5.1",
"builder-util": "22.5.1",
"fs-extra": "^9.0.0",
"iconv-lite": "^0.5.1",
"js-yaml": "^3.13.1",
"sanitize-filename": "^1.6.3"
},
"dependencies": {
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"iconv-lite": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.1.tgz",
@ -4927,14 +4904,14 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"ejs": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.0.2.tgz",
"integrity": "sha512-IncmUpn1yN84hy2shb0POJ80FWrfGNY0cxO9f4v+/sG7qcBvAtVWUA1IdzY/8EYUmOVhoKJVdJjNd3AZcnxOjA=="
},
"electron": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-8.2.1.tgz",
"integrity": "sha512-+1PispFqjyKj3VeOPbEKEl6LYxPW41OxHgh9CGN8KeGygsKDHSZuuG9rYc+b9NeeaAl+gnV9VO2JOe7BIzXyOg==",
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-8.2.3.tgz",
"integrity": "sha512-FJUp103c8yJBoAaj/QM/OBde57iJh95u1yGJBytMUXmLFSsx78LmNE03QN4XCODyzi76IEcasvUcK6scogRLbQ==",
"requires": {
"@electron/get": "^1.0.1",
"@types/node": "^12.0.12",
@ -4942,24 +4919,24 @@
}
},
"electron-builder": {
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-22.4.1.tgz",
"integrity": "sha512-13CjZcGeJS+c3EKRwFT/Oty5Niif5g1FwDioBLEbjkPCPQgxdtDsr+rJtCu9qxkiKDYpAoPS+t/clNk0efONvQ==",
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-22.5.1.tgz",
"integrity": "sha512-7gnHN8Ml5zecDerN8/ljAwUKtE+hhGLuT/X2/zO0FJM2q2hlLx/6ZgzESFILKqnPQFEBRxQ8SL1OxjdIY0HIrw==",
"requires": {
"@types/yargs": "^15.0.4",
"app-builder-lib": "22.4.1",
"app-builder-lib": "22.5.1",
"bluebird-lst": "^1.0.9",
"builder-util": "22.4.1",
"builder-util-runtime": "8.6.2",
"builder-util": "22.5.1",
"builder-util-runtime": "8.7.0",
"chalk": "^3.0.0",
"dmg-builder": "22.4.1",
"fs-extra": "^8.1.0",
"dmg-builder": "22.5.1",
"fs-extra": "^9.0.0",
"is-ci": "^2.0.0",
"lazy-val": "^1.0.4",
"read-config-file": "5.0.2",
"read-config-file": "6.0.0",
"sanitize-filename": "^1.6.3",
"update-notifier": "^4.1.0",
"yargs": "^15.1.0"
"yargs": "^15.3.1"
},
"dependencies": {
"ansi-regex": {
@ -5022,16 +4999,6 @@
"path-exists": "^4.0.0"
}
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -5103,9 +5070,9 @@
}
},
"yargs-parser": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.1.tgz",
"integrity": "sha512-KRHEsOM16IX7XuLnMOqImcPNbLVXMNHYAoFc3BKR8Ortl5gzDbtXvvEoGx9imk5E+X1VeNKNlcHr8B8vi+7ipA==",
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
@ -5204,16 +5171,16 @@
}
},
"electron-publish": {
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.4.1.tgz",
"integrity": "sha512-nwKNum3KXm+01rtWX2pc1jhazdzDy2zYnQx+zmXphZchjd6UOMX3ZN0xyZUCKugw5ZliflT6LkgbrcBXBtYD3A==",
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.5.1.tgz",
"integrity": "sha512-g5bwLAHZT6A++yU1+Et+fncnFAdXXgkRao9rzTFAvhQ0QJBsmLiyOd0Ta2RI/EQcVoy6jyHtxFs7CWIXE5aZOA==",
"requires": {
"@types/fs-extra": "^8.1.0",
"bluebird-lst": "^1.0.9",
"builder-util": "~22.4.1",
"builder-util-runtime": "8.6.2",
"builder-util": "22.5.1",
"builder-util-runtime": "8.7.0",
"chalk": "^3.0.0",
"fs-extra": "^8.1.0",
"fs-extra": "^9.0.0",
"lazy-val": "^1.0.4",
"mime": "^2.4.4"
},
@ -5249,16 +5216,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -5766,9 +5723,9 @@
}
},
"eslint-plugin-prettier": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
"integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz",
"integrity": "sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==",
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
@ -6083,14 +6040,14 @@
}
},
"extract-zip": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
"integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
"integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
"requires": {
"concat-stream": "1.6.2",
"debug": "2.6.9",
"mkdirp": "0.5.1",
"yauzl": "2.4.1"
"concat-stream": "^1.6.2",
"debug": "^2.6.9",
"mkdirp": "^0.5.4",
"yauzl": "^2.10.0"
},
"dependencies": {
"debug": {
@ -6101,6 +6058,19 @@
"ms": "2.0.0"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -6147,9 +6117,9 @@
}
},
"fd-slicer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"requires": {
"pend": "~1.2.0"
}
@ -8763,9 +8733,9 @@
}
},
"is-installed-globally": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.1.tgz",
"integrity": "sha512-oiEcGoQbGc+3/iijAijrK2qFpkNoNjsHOm/5V5iaeydyrS/hnwaRCEgH5cpW0P3T1lSjV5piB7S5b5lEugNLhg==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz",
"integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==",
"requires": {
"global-dirs": "^2.0.1",
"is-path-inside": "^3.0.1"
@ -9075,11 +9045,18 @@
"integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA=="
},
"json5": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz",
"integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"requires": {
"minimist": "^1.2.0"
"minimist": "^1.2.5"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}
}
},
"jsonfile": {
@ -9133,9 +9110,9 @@
}
},
"keytar": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-5.4.0.tgz",
"integrity": "sha512-Ta0RtUmkq7un177SPgXKQ7FGfGDV4xvsV0cGNiWVEzash5U0wyOsXpwfrK2+Oq+hHvsvsbzIZUUuJPimm3avFw==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-5.5.0.tgz",
"integrity": "sha512-1d/F2qAL/qijpm25wNq8eez4mE+/J4eBvqyLfspIYIeCEHn3nttFwplowIZfbdNCjFGWh68MzGZOd2vS61Ffew==",
"optional": true,
"requires": {
"nan": "2.14.0",
@ -11853,12 +11830,12 @@
}
},
"raw-loader": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.0.tgz",
"integrity": "sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.1.tgz",
"integrity": "sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A==",
"requires": {
"loader-utils": "^1.2.3",
"schema-utils": "^2.5.0"
"loader-utils": "^2.0.0",
"schema-utils": "^2.6.5"
},
"dependencies": {
"ajv": {
@ -11872,10 +11849,25 @@
"uri-js": "^4.2.2"
}
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz",
"integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz",
"integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==",
"requires": {
"ajv": "^6.12.0",
"ajv-keywords": "^3.4.1"
@ -11907,28 +11899,15 @@
"integrity": "sha512-Nrd/65LzMjFmKpS9d2fqIxVYdW0M8ovsN0PgZhCrPMQss2yznkp6/zjEQ1a9DzzoGv2uuN3yDJAeHybOD5ZNKA=="
},
"read-config-file": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-5.0.2.tgz",
"integrity": "sha512-tVt1lsiSjs+FtL/vtfCivqtKR1UNk3BB3uPJQvJqkgtAYDvZjo0xyXFYSVmzaTcO+Jdi5G7O2K2vDV+p1M/oug==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.0.0.tgz",
"integrity": "sha512-PHjROSdpceKUmqS06wqwP92VrM46PZSTubmNIMJ5DrMwg1OgenSTSEHIkCa6TiOJ+y/J0xnG1fFwG3M+Oi1aNA==",
"requires": {
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"fs-extra": "^8.1.0",
"js-yaml": "^3.13.1",
"json5": "^2.1.1",
"json5": "^2.1.2",
"lazy-val": "^1.0.4"
},
"dependencies": {
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
}
}
},
"read-pkg": {
@ -15598,9 +15577,9 @@
}
},
"webpack-bundle-analyzer": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.1.tgz",
"integrity": "sha512-Nfd8HDwfSx1xBwC+P8QMGvHAOITxNBSvu/J/mCJvOwv+G4VWkU7zir9SSenTtyCi0LnVtmsc7G5SZo1uV+bxRw==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.7.0.tgz",
"integrity": "sha512-mETdjZ30a3Yf+NTB/wqTgACK7rAYQl5uxKK0WVTNmF0sM3Uv8s3R58YZMW7Rhu0Lk2Rmuhdj5dcH5Q76zCDVdA==",
"requires": {
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1",
@ -15617,10 +15596,10 @@
"ws": "^6.0.0"
},
"dependencies": {
"acorn": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg=="
"ejs": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
}
}
},
@ -16498,11 +16477,12 @@
}
},
"yauzl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
"integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"requires": {
"fd-slicer": "~1.0.1"
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"zip-stream": {

View File

@ -1,6 +1,6 @@
{
"name": "keeweb",
"version": "1.13.4",
"version": "1.14.0",
"description": "Free cross-platform password manager compatible with KeePass",
"main": "Gruntfile.js",
"private": true,
@ -28,14 +28,14 @@
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"dompurify": "^2.0.8",
"electron": "^8.2.1",
"electron-builder": "^22.4.1",
"electron": "^8.2.3",
"electron-builder": "^22.5.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.1",
"exports-loader": "0.7.0",
@ -78,7 +78,7 @@
"postcss-loader": "^3.0.0",
"prettier": "^1.19.1",
"puppeteer": "^2.1.1",
"raw-loader": "^4.0.0",
"raw-loader": "^4.0.1",
"run-remote-task": "^0.3.0",
"sass-loader": "^8.0.2",
"stats-webpack-plugin": "0.7.0",
@ -89,12 +89,12 @@
"time-grunt": "2.0.0",
"url-loader": "^4.1.0",
"webpack": "^4.42.1",
"webpack-bundle-analyzer": "^3.6.1",
"webpack-bundle-analyzer": "^3.7.0",
"webpack-dev-server": "^3.10.3"
},
"optionalDependencies": {
"grunt-appdmg": "github:keeweb/grunt-appdmg#874ad83",
"keytar": "^5.4.0"
"keytar": "^5.5.0"
},
"scripts": {
"start": "grunt",

View File

@ -1,5 +1,13 @@
Release notes
-------------
##### v1.14.0 (2020-04-18)
`+` using OAuth authorization code grant for all storage providers
`-` fixed a number of vulnerabilities in opening untrusted kdbx files
`+` applied recommendations from the electron security checklist
`*` canOpenWebdav is now canOpenStorage
`+` option to log out from storages
`*` saving only modified settings instead of everything
##### v1.13.4 (2020-04-15)
`-` fix #1457: fixed styles in theme plugins
`+` #1456: options to hide webdav and password generator

View File

@ -3,20 +3,21 @@ import { MdToHtml } from 'util/formatting/md-to-html';
describe('MdToHtml', () => {
it('should convert markdown', () => {
expect(MdToHtml.convert('## head\n_italic_')).to.eql(
'<div class="markdown"><h2>head</h2>\n<p><em>italic</em></p>\n</div>'
);
expect(MdToHtml.convert('## head\n_italic_')).to.eql({
html: '<div class="markdown"><h2>head</h2>\n<p><em>italic</em></p>\n</div>'
});
});
it('should not add markdown wrapper tags for plaintext', () => {
expect(MdToHtml.convert('plain\ntext')).to.eql('plain\ntext');
expect(MdToHtml.convert('plain\ntext')).to.eql({ text: 'plain\ntext' });
});
it('should convert links', () => {
expect(MdToHtml.convert('[link](https://x)')).to.eql(
'<div class="markdown">' +
expect(MdToHtml.convert('[link](https://x)')).to.eql({
html:
'<div class="markdown">' +
'<p><a href="https://x" rel="noreferrer noopener" target="_blank">link</a></p>\n' +
'</div>'
);
});
});
});

View File

@ -32,4 +32,13 @@ describe('UrlFormat', () => {
})
).to.eql('/path?hello=world&data=%3D%20%26');
});
it('should make form-data params', () => {
expect(
UrlFormat.buildFormData({
hello: 'world',
data: '= &'
})
).to.eql('hello=world&data=%3D%20%26');
});
});