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

Merge branch 'dev' into release-2.3

This commit is contained in:
Laurent Cozic 2021-08-15 12:15:28 +01:00
commit 2a732ba03d
32 changed files with 825 additions and 562 deletions

View File

@ -7,4 +7,6 @@ packages/app-cli
packages/app-mobile
packages/app-clipper
packages/generator-joplin
packages/plugin-repo-cli
packages/plugin-repo-cli
packages/server/db-*.sqlite
packages/server/temp

View File

@ -134,7 +134,7 @@ if [[ $GIT_TAG_NAME = v* ]]; then
elif [[ $GIT_TAG_NAME = server-v* ]] && [[ $IS_LINUX = 1 ]]; then
echo "Step: Building Docker Image..."
cd "$ROOT_DIR"
npm run buildServerDocker -- --tag-name $GIT_TAG_NAME
npm run buildServerDocker -- --tag-name $GIT_TAG_NAME --push-images
else
echo "Step: Building but *not* publishing desktop application..."
USE_HARD_LINKS=false npm run dist -- --publish=never

View File

@ -33,10 +33,11 @@ const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import { reg } from '@joplin/lib/registry';
import ErrorBoundary from '../../../ErrorBoundary';
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
const menuUtils = new MenuUtils(CommandService.instance());
function markupRenderOptions(override: any = null) {
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
return { ...override };
}
@ -384,6 +385,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
monospaceFonts.push('monospace');
const maxWidthCss = props.contentMaxWidth ? `
margin-right: auto !important;
margin-left: auto !important;
max-width: ${props.contentMaxWidth}px !important;
` : '';
const element = document.createElement('style');
element.setAttribute('id', 'codemirrorStyle');
document.head.appendChild(element);
@ -418,6 +425,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
/* Add a fixed right padding to account for the appearance (and disappearance) */
/* of the sidebar */
padding-right: 10px !important;
${maxWidthCss}
}
/* This enforces monospace for certain elements (code, tables, etc.) */
@ -467,6 +475,20 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
color: ${theme.codeColor};
}
div.CodeMirror span.cm-comment.cm-jn-inline-code {
border: 1px solid ${theme.codeBorderColor};
background-color: ${theme.codeBackgroundColor};
padding-right: .2em;
padding-left: .2em;
border-radius: .25em;
}
div.CodeMirror pre.cm-jn-code-block {
background-color: ${theme.codeBackgroundColor};
padding-right: .2em;
padding-left: .2em;
}
div.CodeMirror span.cm-strong {
color: ${theme.colorBright};
}
@ -533,7 +555,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return () => {
document.head.removeChild(element);
};
}, [props.themeId]);
}, [props.themeId, props.contentMaxWidth]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
@ -572,7 +594,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
}
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({ resourceInfos: props.resourceInfos }));
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({
resourceInfos: props.resourceInfos,
contentMaxWidth: props.contentMaxWidth,
}));
if (cancelled) return;
@ -795,6 +820,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
viewerStyle={styles.viewer}
onIpcMessage={webview_ipcMessage}
onDomReady={webview_domReady}
contentMaxWidth={props.contentMaxWidth}
/>
</div>
);

View File

@ -7,6 +7,7 @@ interface JoplinModeState {
outer: any;
openCharacter: string;
inTable: boolean;
inCodeBlock: boolean;
inner: any;
}
@ -48,6 +49,7 @@ export default function useJoplinMode(CodeMirror: any) {
outer: CodeMirror.startState(markdownMode),
openCharacter: '',
inTable: false,
inCodeBlock: false,
inner: CodeMirror.startState(stex),
};
},
@ -57,6 +59,7 @@ export default function useJoplinMode(CodeMirror: any) {
outer: CodeMirror.copyState(markdownMode, state.outer),
openCharacter: state.openCharacter,
inTable: state.inTable,
inCodeBlock: state.inCodeBlock,
inner: CodeMirror.copyState(stex, state.inner),
};
},
@ -115,9 +118,26 @@ export default function useJoplinMode(CodeMirror: any) {
let isMonospace = false;
// After being passed to the markdown mode we can check if the
// code state variables are set
// Code Block
if (state.outer.code || (state.outer.thisLine && state.outer.thisLine.fencedCodeEnd)) {
// Code
if (state.outer.code > 0) {
// state.outer.code holds the number of preceding backticks
// anything > 0 backticks is an inline-code-block
// -1 is used for actual code blocks
isMonospace = true;
token = `${token} jn-inline-code`;
} else if (state.outer.thisLine && state.outer.thisLine.fencedCodeEnd) {
state.inCodeBlock = false;
isMonospace = true;
token = `${token} line-cm-jn-code-block`;
} else if (state.outer.code === -1 || state.inCodeBlock) {
state.inCodeBlock = true;
isMonospace = true;
token = `${token} line-cm-jn-code-block`;
} else if (stream.pos > 0 && stream.string[stream.pos - 1] === '`' &&
!!token && token.includes('comment')) {
// This grabs the closing backtick for inline Code
isMonospace = true;
token = `${token} jn-inline-code`;
}
// Indented Code
if (state.outer.indentedCode) {
@ -163,6 +183,10 @@ export default function useJoplinMode(CodeMirror: any) {
}
state.inTable = false;
if (state.inCodeBlock) return 'line-cm-jn-code-block';
return null;
},
electricChars: markdownMode.electricChars,

View File

@ -14,18 +14,18 @@ import { _, closestSupportedLocale } from '@joplin/lib/locale';
import useContextMenu from './utils/useContextMenu';
import { copyHtmlToClipboard } from '../../utils/clipboardUtils';
import shim from '@joplin/lib/shim';
const { MarkupToHtml } = require('@joplin/renderer');
import { MarkupToHtml } from '@joplin/renderer';
import { reg } from '@joplin/lib/registry';
import BaseItem from '@joplin/lib/models/BaseItem';
import setupToolbarButtons from './utils/setupToolbarButtons';
import { plainTextToHtml } from '@joplin/lib/htmlUtils';
import openEditDialog from './utils/openEditDialog';
const { themeStyle } = require('@joplin/lib/theme');
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
import { themeStyle } from '@joplin/lib/theme';
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
function markupRenderOptions(override: any = null) {
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
return {
plugins: {
checkbox: {
@ -148,8 +148,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
if (!resourceMd) return;
const result = await props.markupToHtml(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMd, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html);
// editor.fire('joplinChange');
// dispatchDidUpdate(editor);
}, [props.markupToHtml, editor]);
const insertResourcesIntoContentRef = useRef(null);

View File

@ -159,8 +159,8 @@ function NoteEditor(props: NoteEditorProps) {
customCss: props.customCss,
});
return markupToHtml.allAssets(markupLanguage, theme);
}, [props.themeId, props.customCss]);
return markupToHtml.allAssets(markupLanguage, theme, { contentMaxWidth: props.contentMaxWidth });
}, [props.themeId, props.customCss, props.contentMaxWidth]);
const handleProvisionalFlag = useCallback(() => {
if (props.isProvisional) {
@ -400,6 +400,7 @@ function NoteEditor(props: NoteEditorProps) {
noteToolbarButtonInfos: props.toolbarButtonInfos,
plugins: props.plugins,
fontSize: Setting.value('style.editor.fontSize'),
contentMaxWidth: props.contentMaxWidth,
};
let editor = null;
@ -601,6 +602,7 @@ const mapStateToProps = (state: AppState) => {
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([
'setTags',
], whenClauseContext)[0],
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
};
};

View File

@ -4,6 +4,7 @@ import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUt
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { MarkupLanguage } from '@joplin/renderer';
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
import { MarkupToHtmlOptions } from './useMarkupToHtml';
export interface ToolbarButtonInfos {
[key: string]: ToolbarButtonInfo;
@ -37,6 +38,7 @@ export interface NoteEditorProps {
toolbarButtonInfos: ToolbarButtonInfo[];
setTagsToolbarButtonInfo: ToolbarButtonInfo;
richTextBannerDismissed: boolean;
contentMaxWidth: number;
}
export interface NoteBodyEditorProps {
@ -51,7 +53,7 @@ export interface NoteBodyEditorProps {
onWillChange(event: any): void;
onMessage(event: any): void;
onScroll(event: any): void;
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: any)=> Promise<RenderResult>;
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
htmlToMarkdown: Function;
allAssets: (markupLanguage: MarkupLanguage)=> Promise<RenderResultPluginAsset[]>;
disabled: boolean;
@ -67,6 +69,7 @@ export interface NoteBodyEditorProps {
noteToolbarButtonInfos: ToolbarButtonInfo[];
plugins: PluginStates;
fontSize: number;
contentMaxWidth: number;
}
export interface FormNote {

View File

@ -13,9 +13,12 @@ interface HookDependencies {
plugins: PluginStates;
}
interface MarkupToHtmlOptions {
export interface MarkupToHtmlOptions {
replaceResourceInternalToExternalLinks?: boolean;
resourceInfos?: ResourceInfos;
contentMaxWidth?: number;
plugins?: Record<string, any>;
bodyOnly?: boolean;
}
export default function useMarkupToHtml(deps: HookDependencies) {

View File

@ -972,6 +972,8 @@ class Setting extends BaseModel {
storage: SettingStorage.File,
},
'style.editor.contentMaxWidth': { value: 600, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space.') },
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
// TODO: Is there a better way to do this? The goal here is to simply have

View File

@ -1,6 +1,7 @@
import MdToHtml from './MdToHtml';
import HtmlToHtml from './HtmlToHtml';
import htmlUtils from './htmlUtils';
import { Options as NoteStyleOptions } from './noteStyle';
const MarkdownIt = require('markdown-it');
export enum MarkupLanguage {
@ -48,7 +49,7 @@ export default class MarkupToHtml {
private options_: Options;
private rawMarkdownIt_: any;
public constructor(options: Options) {
public constructor(options: Options = null) {
this.options_ = {
ResourceModel: {
isResourceUrl: () => false,
@ -119,7 +120,7 @@ export default class MarkupToHtml {
return this.renderer(markupLanguage).render(markup, theme, options);
}
public async allAssets(markupLanguage: MarkupLanguage, theme: any) {
return this.renderer(markupLanguage).allAssets(theme);
public async allAssets(markupLanguage: MarkupLanguage, theme: any, noteStyleOptions: NoteStyleOptions = null) {
return this.renderer(markupLanguage).allAssets(theme, noteStyleOptions);
}
}

View File

@ -5,10 +5,28 @@ import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks';
import { ItemIdToUrlHandler } from './utils';
import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
import { Options as NoteStyleOptions } from './noteStyle';
const MarkdownIt = require('markdown-it');
const md5 = require('md5');
export interface RenderOptions {
contentMaxWidth?: number;
bodyOnly?: boolean;
splitted?: boolean;
externalAssetsOnly?: boolean;
postMessageSyntax?: string;
highlightedKeywords?: string[];
codeTheme?: string;
theme?: any;
plugins?: Record<string, any>;
audioPlayerEnabled?: boolean;
videoPlayerEnabled?: boolean;
pdfViewerEnabled?: boolean;
codeHighlightCacheKey?: string;
plainResourceRendering?: boolean;
}
interface RendererRule {
install(context: any, ruleOptions: any): any;
assets?(theme: any): any;
@ -331,7 +349,7 @@ export default class MdToHtml {
}
// This is similar to allProcessedAssets() but used only by the Rich Text editor
public async allAssets(theme: any) {
public async allAssets(theme: any, noteStyleOptions: NoteStyleOptions = null) {
const assets: any = {};
for (const key in rules) {
if (!this.pluginEnabled(key)) continue;
@ -343,7 +361,7 @@ export default class MdToHtml {
}
const processedAssets = this.processPluginAssets(assets);
processedAssets.cssStrings.splice(0, 0, noteStyle(theme).join('\n'));
processedAssets.cssStrings.splice(0, 0, noteStyle(theme, noteStyleOptions).join('\n'));
if (this.customCss_) processedAssets.cssStrings.push(this.customCss_);
const output = await this.outputAssetsToExternalAssets_(processedAssets);
return output.pluginAssets;
@ -376,8 +394,9 @@ export default class MdToHtml {
}
// "theme" is the theme as returned by themeStyle()
public async render(body: string, theme: any = null, options: any = null): Promise<RenderResult> {
options = Object.assign({}, {
public async render(body: string, theme: any = null, options: RenderOptions = null): Promise<RenderResult> {
options = {
// In bodyOnly mode, the rendered Markdown is returned without the wrapper DIV
bodyOnly: false,
// In splitted mode, the CSS and HTML will be returned in separate properties.
@ -395,7 +414,10 @@ export default class MdToHtml {
audioPlayerEnabled: this.pluginEnabled('audioPlayer'),
videoPlayerEnabled: this.pluginEnabled('videoPlayer'),
pdfViewerEnabled: this.pluginEnabled('pdfViewer'),
}, options);
contentMaxWidth: 0,
...options,
};
// The "codeHighlightCacheKey" option indicates what set of cached object should be
// associated with this particular Markdown body. It is only used to allow us to
@ -525,7 +547,9 @@ export default class MdToHtml {
const renderedBody = markdownIt.render(body, context);
let cssStrings = noteStyle(options.theme);
let cssStrings = noteStyle(options.theme, {
contentMaxWidth: options.contentMaxWidth,
});
let output = { ...this.allProcessedAssets(allRules, options.theme, options.codeTheme) };
cssStrings = cssStrings.concat(output.cssStrings);

View File

@ -7,11 +7,28 @@ function formatCssSize(v: any): string {
return `${v}px`;
}
export default function(theme: any) {
export interface Options {
contentMaxWidth?: number;
}
export default function(theme: any, options: Options = null) {
options = {
contentMaxWidth: 0,
...options,
};
theme = theme ? theme : {};
const fontFamily = '\'Avenir\', \'Arial\', sans-serif';
const maxWidthCss = options.contentMaxWidth ? `
#rendered-md {
max-width: ${options.contentMaxWidth}px;
margin-left: auto;
margin-right: auto;
}
` : '';
const css =
`
/* https://necolas.github.io/normalize.css/ */
@ -61,6 +78,8 @@ export default function(theme: any) {
background: rgba(100, 100, 100, 0.7);
}
${maxWidthCss}
/* Remove top padding and margin from first child so that top of rendered text is aligned to top of text editor text */
#rendered-md > h1:first-child,

View File

@ -1,3 +1,5 @@
// We don't want the tests to fail due to timeout, especially on CI, and certain
// tests can take more time since we do integration testing too.
jest.setTimeout(30 * 1000);
process.env.JOPLIN_IS_TESTING = '1';

View File

@ -31,6 +31,7 @@
"pg": "^8.5.1",
"pretty-bytes": "^5.6.0",
"query-string": "^6.8.3",
"rate-limiter-flexible": "^2.2.4",
"raw-body": "^2.4.1",
"sqlite3": "^4.1.0",
"stripe": "^8.150.0",
@ -8802,6 +8803,11 @@
"node": ">= 0.6"
}
},
"node_modules/rate-limiter-flexible": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz",
"integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw=="
},
"node_modules/raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
@ -17770,6 +17776,11 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true
},
"rate-limiter-flexible": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz",
"integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw=="
},
"raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",

View File

@ -4,11 +4,12 @@
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
"start-dev-no-watch": "node dist/app.js --env dev",
"devCreateDb": "node dist/app.js --env dev --create-db",
"devDropTables": "node dist/app.js --env dev --drop-tables",
"devDropDb": "node dist/app.js --env dev --drop-db",
"start": "node dist/app.js",
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-latest --env buildTypes && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
"tsc": "tsc --project tsconfig.json",
"test": "jest --verbose=false",
"test-ci": "npm run test",
@ -42,6 +43,7 @@
"pg": "^8.5.1",
"pretty-bytes": "^5.6.0",
"query-string": "^6.8.3",
"rate-limiter-flexible": "^2.2.4",
"raw-body": "^2.4.1",
"sqlite3": "^4.1.0",
"stripe": "^8.150.0",

View File

@ -7,7 +7,7 @@ import { argv } from 'yargs';
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, runningInDocker, EnvVariables } from './config';
import { createDb, dropDb } from './tools/dbTools';
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteDefaultDir } from './db';
import { dropTables, connectDb, disconnectDb, migrateLatest, waitForConnection, sqliteDefaultDir, migrateList, migrateUp, migrateDown } from './db';
import { AppContext, Env, KoaNext } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
import routeHandler from './middleware/routeHandler';
@ -205,10 +205,23 @@ async function main() {
fs.writeFileSync(pidFile, `${process.pid}`);
}
if (argv.migrateDb) {
let runCommandAndExitApp = true;
if (argv.migrateLatest) {
const db = await connectDb(config().database);
await migrateDb(db);
await migrateLatest(db);
await disconnectDb(db);
} else if (argv.migrateUp) {
const db = await connectDb(config().database);
await migrateUp(db);
await disconnectDb(db);
} else if (argv.migrateDown) {
const db = await connectDb(config().database);
await migrateDown(db);
await disconnectDb(db);
} else if (argv.migrateList) {
const db = await connectDb(config().database);
console.info(await migrateList(db));
} else if (argv.dropDb) {
await dropDb(config().database, { ignoreIfNotExists: true });
} else if (argv.dropTables) {
@ -218,6 +231,8 @@ async function main() {
} else if (argv.createDb) {
await createDb(config().database);
} else {
runCommandAndExitApp = false;
appLogger().info(`Starting server v${config().appVersion} (${env}) on port ${config().port} and PID ${process.pid}...`);
appLogger().info('Running in Docker:', runningInDocker());
appLogger().info('Public base URL:', config().baseUrl);
@ -239,7 +254,7 @@ async function main() {
await initializeJoplinUtils(config(), ctx.joplinBase.models, ctx.joplinBase.services.mustache);
appLogger().info('Migrating database...');
await migrateDb(ctx.joplinBase.db);
await migrateLatest(ctx.joplinBase.db);
appLogger().info('Starting services...');
await startServices(ctx.joplinBase.services);
@ -248,6 +263,8 @@ async function main() {
app.listen(config().port);
}
if (runCommandAndExitApp) process.exit(0);
}
main().catch((error: any) => {

View File

@ -121,14 +121,85 @@ export async function disconnectDb(db: DbConnection) {
await db.destroy();
}
export async function migrateDb(db: DbConnection) {
export async function migrateLatest(db: DbConnection) {
await db.migrate.latest({
directory: migrationDir,
// Disable transactions because the models might open one too
disableTransactions: true,
});
}
export async function migrateUp(db: DbConnection) {
await db.migrate.up({
directory: migrationDir,
});
}
export async function migrateDown(db: DbConnection) {
await db.migrate.down({
directory: migrationDir,
});
}
export async function migrateList(db: DbConnection, asString: boolean = true) {
const migrations: any = await db.migrate.list({
directory: migrationDir,
});
// The migration array has a rather inconsistent format:
//
// [
// // Done migrations
// [
// '20210809222118_email_key_fix.js',
// '20210814123815_testing.js',
// '20210814123816_testing.js'
// ],
// // Not done migrations
// [
// {
// file: '20210814123817_testing.js',
// directory: '/path/to/packages/server/dist/migrations'
// }
// ]
// ]
if (!asString) return migrations;
const formatName = (migrationInfo: any) => {
const name = migrationInfo.file ? migrationInfo.file : migrationInfo;
const s = name.split('.');
s.pop();
return s.join('.');
};
interface Line {
text: string;
done: boolean;
}
const output: Line[] = [];
for (const s of migrations[0]) {
output.push({
text: formatName(s),
done: true,
});
}
for (const s of migrations[1]) {
output.push({
text: formatName(s),
done: false,
});
}
output.sort((a, b) => {
return a.text < b.text ? -1 : +1;
});
return output.map(l => `${l.done ? '✓' : '✗'} ${l.text}`).join('\n');
}
function allTableNames(): string[] {
const tableNames = Object.keys(databaseSchema);
tableNames.push('knex_migrations');

View File

@ -38,6 +38,8 @@ export default async function(ctx: AppContext) {
const responseFormat = routeResponseFormat(ctx);
if (error.retryAfterMs) ctx.response.set('Retry-After', Math.ceil(error.retryAfterMs / 1000).toString());
if (error.code === 'invalidOrigin') {
ctx.response.body = error.message;
} else if (responseFormat === RouteResponseFormat.Html) {

View File

@ -1,14 +1,14 @@
import { Knex } from 'knex';
// import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
try {
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
table.dropUnique(['recipient_email', 'key']);
});
} catch (error) {
console.warn('Could not drop unique constraint - this is not an error.', error);
}
export async function up(_db: DbConnection): Promise<any> {
// try {
// await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
// table.dropUnique(['recipient_email', 'key']);
// });
// } catch (error) {
// // console.warn('Could not drop unique constraint - this is not an error.', error);
// }
}
export async function down(_db: DbConnection): Promise<any> {

View File

@ -5,12 +5,15 @@ import { ErrorForbidden } from '../../utils/errors';
import { AppContext } from '../../utils/types';
import { bodyFields } from '../../utils/requestUtils';
import { User } from '../../db';
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
const router = new Router(RouteType.Api);
router.public = true;
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
await limiterLoginBruteForce(ctx.ip);
const fields: User = await bodyFields(ctx.req);
const user = await ctx.joplin.models.user().login(fields.email, fields.password);
if (!user) throw new ErrorForbidden('Invalid username or password');

View File

@ -6,6 +6,7 @@ import { formParse } from '../../utils/requestUtils';
import config from '../../config';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
function makeView(error: any = null): View {
const view = defaultView('login', 'Login');
@ -25,6 +26,8 @@ router.get('login', async (_path: SubPath, _ctx: AppContext) => {
});
router.post('login', async (_path: SubPath, ctx: AppContext) => {
await limiterLoginBruteForce(ctx.ip);
try {
const body = await formParse(ctx.req);

View File

@ -1,4 +1,4 @@
import { connectDb, disconnectDb, migrateDb } from '../db';
import { connectDb, disconnectDb, migrateLatest } from '../db';
import * as fs from 'fs-extra';
import { DatabaseConfig } from '../utils/types';
@ -46,7 +46,7 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
try {
const db = await connectDb(config);
await migrateDb(db);
await migrateLatest(db);
await disconnectDb(db);
} catch (error) {
error.message += `: ${config.name}`;

View File

@ -1,4 +1,4 @@
import { DbConnection, dropTables, migrateDb } from '../db';
import { DbConnection, dropTables, migrateLatest } from '../db';
import newModelFactory from '../models/factory';
import { AccountType } from '../models/UserModel';
import { Config } from '../utils/types';
@ -15,7 +15,7 @@ export async function handleDebugCommands(argv: any, db: DbConnection, config: C
export async function createTestUsers(db: DbConnection, config: Config) {
await dropTables(db);
await migrateDb(db);
await migrateLatest(db);
const password = 'hunter1hunter2hunter3';
const models = newModelFactory(db, config);

View File

@ -97,6 +97,17 @@ export class ErrorPayloadTooLarge extends ApiError {
}
}
export class ErrorTooManyRequests extends ApiError {
public static httpCode: number = 429;
public retryAfterMs: number = 0;
public constructor(message: string = null, retryAfterMs: number = 0) {
super(message === null ? 'Too Many Requests' : message, ErrorTooManyRequests.httpCode);
this.retryAfterMs = retryAfterMs;
Object.setPrototypeOf(this, ErrorTooManyRequests.prototype);
}
}
export function errorToString(error: Error): string {
const msg: string[] = [];
msg.push(error.message ? error.message : 'Unknown error');

View File

@ -0,0 +1,19 @@
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
import { ErrorTooManyRequests } from '../errors';
const limiterSlowBruteByIP = new RateLimiterMemory({
points: 3, // Up to 3 requests per IP
duration: 30, // Per 30 seconds
});
export default async function(ip: string) {
// Tests need to make many requests quickly so we disable it in this case.
if (process.env.JOPLIN_IS_TESTING === '1') return;
try {
await limiterSlowBruteByIP.consume(ip);
} catch (error) {
const result = error as RateLimiterRes;
throw new ErrorTooManyRequests(`Too many login attempts. Please try again in ${Math.ceil(result.msBeforeNext / 1000)} seconds.`, result.msBeforeNext);
}
}

View File

@ -106,9 +106,9 @@ async function main() {
fs.removeSync(`${serverRoot}/db-testing.sqlite`);
// const migrateCommand = 'NODE_ENV=testing node dist/app.js --migrate-db --env dev';
// const migrateCommand = 'NODE_ENV=testing node dist/app.js --migrate-latest --env dev';
const clearCommand = 'node dist/app.js --env dev --drop-tables';
const migrateCommand = 'node dist/app.js --env dev --migrate-db';
const migrateCommand = 'node dist/app.js --env dev --migrate-latest';
await execCommand(clearCommand);
await execCommand(migrateCommand);

View File

@ -0,0 +1,11 @@
import { Day, Month, Second } from './time';
describe('time', function() {
it('should have correct interval durations', async function() {
expect(Second).toBe(1000);
expect(Day).toBe(86400000);
expect(Month).toBe(2592000000);
});
});

View File

@ -14,7 +14,7 @@ function initDayJs() {
initDayJs();
export const Second = 60 * 1000;
export const Second = 1000;
export const Minute = 60 * Second;
export const Hour = 60 * Minute;
export const Day = 24 * Hour;

View File

@ -16,6 +16,7 @@ async function main() {
const argv = require('yargs').argv;
if (!argv.tagName) throw new Error('--tag-name not provided');
const pushImages = !!argv.pushImages;
const tagName = argv.tagName;
const isPreRelease = getIsPreRelease(tagName);
const imageVersion = getVersionFromTag(tagName, isPreRelease);
@ -38,6 +39,7 @@ async function main() {
console.info(`Running from: ${process.cwd()}`);
console.info('tagName:', tagName);
console.info('pushImages:', pushImages);
console.info('imageVersion:', imageVersion);
console.info('isPreRelease:', isPreRelease);
console.info('Docker tags:', dockerTags.join(', '));
@ -45,7 +47,7 @@ async function main() {
await execCommand2(`docker build -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`);
for (const tag of dockerTags) {
await execCommand2(`docker tag "joplin/server:${imageVersion}" "joplin/server:${tag}"`);
await execCommand2(`docker push joplin/server:${tag}`);
if (pushImages) await execCommand2(`docker push joplin/server:${tag}`);
}
}

View File

@ -3153,7 +3153,7 @@ msgstr "titel"
#: packages/lib/models/Folder.js:39 packages/lib/models/Note.js:37
msgid "updated date"
msgstr "uppdaterad datum"
msgstr "uppdaterat datum"
#: packages/lib/models/Folder.js:103
msgid "Conflicts"
@ -3171,7 +3171,7 @@ msgstr ""
#: packages/lib/models/Note.js:38
msgid "created date"
msgstr "skapad datum"
msgstr "skapat datum"
#: packages/lib/models/Note.js:39
msgid "custom order"

File diff suppressed because it is too large Load Diff