CKEditor components replaced by CKEditor 5.
Remains of the sandboxed CKEditor - will be removed, but the version here may be useful for another editor that is prone to XSS (like Summernote).
This commit is contained in:
parent
213039c141
commit
eacdc74c29
43 changed files with 12499 additions and 1382 deletions
3
TODO.md
3
TODO.md
|
@ -10,6 +10,9 @@
|
|||
### Message delivery
|
||||
- Better integration with ZoneMTA to allow multiple send configurations (with different DKIM) against one ZoneMTA instance via different HTTP configuration of ZoneMTA. This may need an extension of ZoneMTA to provide some header entry that identifies the campaign.
|
||||
|
||||
### Lists
|
||||
- CSV Export
|
||||
|
||||
### Campaigns
|
||||
- Statistics for a sent campaign
|
||||
- List of sent RSS campaigns (?)
|
||||
|
|
|
@ -22,7 +22,8 @@ const api = require('./routes/api');
|
|||
// These are routes for the new React-based client
|
||||
const reports = require('./routes/reports');
|
||||
const subscription = require('./routes/subscription');
|
||||
const mosaico = require('./routes/mosaico');
|
||||
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
||||
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
||||
const files = require('./routes/files');
|
||||
const links = require('./routes/links');
|
||||
const archive = require('./routes/archive');
|
||||
|
@ -221,7 +222,8 @@ function createApp(appType) {
|
|||
useWith404Fallback('/files', files);
|
||||
}
|
||||
|
||||
useWith404Fallback('/mosaico', mosaico.getRouter(appType));
|
||||
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
|
||||
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
|
||||
|
||||
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"presets": ["es2015", "stage-1"],
|
||||
"plugins": ["transform-react-jsx", "transform-decorators-legacy", "transform-function-bind"]
|
||||
}
|
5578
client/package-lock.json
generated
5578
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -16,6 +16,24 @@
|
|||
"license": "GPL-3.0",
|
||||
"homepage": "https://mailtrain.org/",
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-alignment": "^10.0.3",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^10.0.3",
|
||||
"@ckeditor/ckeditor5-block-quote": "^10.1.0",
|
||||
"@ckeditor/ckeditor5-core": "^11.0.1",
|
||||
"@ckeditor/ckeditor5-easy-image": "^10.0.3",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^11.0.1",
|
||||
"@ckeditor/ckeditor5-essentials": "^10.1.2",
|
||||
"@ckeditor/ckeditor5-heading": "^10.1.0",
|
||||
"@ckeditor/ckeditor5-image": "^11.0.0",
|
||||
"@ckeditor/ckeditor5-link": "^10.0.4",
|
||||
"@ckeditor/ckeditor5-list": "^11.0.2",
|
||||
"@ckeditor/ckeditor5-media-embed": "^10.0.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^10.0.3",
|
||||
"@ckeditor/ckeditor5-react": "^1.0.0",
|
||||
"@ckeditor/ckeditor5-table": "^11.0.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^11.1.0",
|
||||
"@ckeditor/ckeditor5-ui": "^11.1.0",
|
||||
"@ckeditor/ckeditor5-upload": "^10.0.3",
|
||||
"axios": "^0.16.2",
|
||||
"datatables.net": "^1.10.15",
|
||||
"datatables.net-bs": "^1.10.15",
|
||||
|
@ -28,7 +46,6 @@
|
|||
"querystringify": "^1.0.0",
|
||||
"react": "^15.6.1",
|
||||
"react-ace": "^5.1.0",
|
||||
"react-ckeditor-component": "^1.0.7",
|
||||
"react-day-picker": "^6.1.0",
|
||||
"react-dnd-html5-backend": "^2.4.1",
|
||||
"react-dnd-touch-backend": "^0.3.13",
|
||||
|
@ -41,16 +58,20 @@
|
|||
"url-parse": "^1.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
|
||||
"@ckeditor/ckeditor5-dev-webpack-plugin": "^7.0.1",
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-1": "^6.24.1",
|
||||
"css-loader": "^0.28.4",
|
||||
"i18next-conv": "^3.0.3",
|
||||
"node-sass": "^4.5.3",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"url-loader": "^0.5.9",
|
||||
|
|
|
@ -36,7 +36,8 @@ import {DeleteModalDialog} from "../lib/modals";
|
|||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {
|
||||
getTemplateTypes,
|
||||
getTypeForm
|
||||
getTypeForm,
|
||||
ResourceType
|
||||
} from '../templates/helpers';
|
||||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
|
@ -50,7 +51,6 @@ import {
|
|||
} from "../../../shared/campaigns";
|
||||
import moment from 'moment';
|
||||
import {getMailerTypes} from "../send-configurations/helpers";
|
||||
import {ResourceType} from "../lib/mosaico";
|
||||
import {getCampaignLabels} from "./helpers";
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -21,12 +21,12 @@ import mailtrainConfig from 'mailtrainConfig';
|
|||
import {
|
||||
getEditForm,
|
||||
getTemplateTypes,
|
||||
getTypeForm
|
||||
getTypeForm,
|
||||
ResourceType
|
||||
} from '../templates/helpers';
|
||||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
import {getUrl} from "../lib/urls";
|
||||
import {ResourceType} from "../lib/mosaico";
|
||||
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -58,7 +58,7 @@ export default class List extends Component {
|
|||
actions: data => {
|
||||
const actions = [];
|
||||
|
||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers')) {
|
||||
if (mailtrainConfig.globalPermissions.setupAutomation && this.props.campaign.permissions.includes('manageTriggers')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
||||
|
@ -77,7 +77,7 @@ export default class List extends Component {
|
|||
return (
|
||||
<div>
|
||||
{tableDeleteDialogRender(this, `rest/triggers/${this.props.campaign.id}`, t('Deleting trigger ...'), t('Trigger deleted'))}
|
||||
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers') &&
|
||||
{mailtrainConfig.globalPermissions.setupAutomation && this.props.campaign.permissions.includes('manageTriggers') &&
|
||||
<Toolbar>
|
||||
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
||||
</Toolbar>
|
||||
|
|
16
client/src/lib/ckeditor-insert-image.svg
Normal file
16
client/src/lib/ckeditor-insert-image.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
|
||||
<path
|
||||
style="fill:#000000;fill-rule:nonzero;stroke-width:0.89999998"
|
||||
d="m 4.9296875,1.5273438 c -0.549,0 -0.9902344,0.4233124 -0.9902344,0.9453124 V 8.1542969 C 3.9929718,8.1073482 4.0362288,8.0501836 4.09375,8.0078125 4.4641691,7.6414817 4.869899,7.3679645 5.2890625,7.1503906 V 2.8496094 H 17.501953 V 11.957031 L 15.234375,9.2851562 c -0.220904,-0.204796 -0.562299,-0.204796 -0.783203,0 l -2.00586,1.8535158 -0.451171,-0.421875 c 0.101968,0.808412 -0.008,1.673794 -0.398438,2.529297 -0.04665,0.163457 -0.103202,0.325377 -0.201172,0.464844 l -0.002,0.002 -0.257812,0.36914 h 6.736328 c 0.54,0 0.982422,-0.422125 0.982422,-0.953125 V 2.4726562 c 0,-0.522 -0.442422,-0.9453124 -0.982422,-0.9453124 z M 14.714844,3.8828125 C 13.350658,3.8741006 12.49215,5.3495586 13.173828,6.53125 14.349609,8.5716138 17.410156,6.8079419 16.234375,4.7675781 15.921123,4.2235411 15.342608,3.8866937 14.714844,3.8828125 Z"
|
||||
id="path2"/>
|
||||
<path
|
||||
style="fill:#000000;fill-rule:evenodd;stroke-width:0.69999999"
|
||||
d="m 6.8138243,16.679773 0.6937,-0.9912 a 0.52500478,0.52500478 0 1 1 0.8603,0.602 l -0.8036,1.148 a 0.5236,0.5236 0 0 1 -0.1519,0.1442 3.6757,3.6757 0 0 1 -5.9521,-4.1685 c 0.014,-0.0665 0.042,-0.1323 0.084,-0.1918 l 0.8029,-1.1473 a 0.525,0.525 0 1 1 0.8596,0.602 l -0.6937,0.9926 0.0042,0.0021 a 2.625,2.625 0 0 0 4.2924,3.0058 l 0.0042,0.0028 z m 3.8457997,-3.7345 a 0.5236,0.5236 0 0 1 -0.084,0.1918 l -0.8028997,1.1473 a 0.525,0.525 0 1 1 -0.8596,-0.602 l 0.602,-0.861 a 2.625,2.625 0 0 0 -4.3008,-3.0106998 l -0.602,0.8602998 a 0.52500478,0.52500478 0 0 1 -0.8603,-0.602 l 0.8036,-1.1479998 a 0.5236,0.5236 0 0 1 0.1519,-0.1442 3.6757,3.6757 0 0 1 5.9520997,4.1684998 z m -3.1940997,-1.7724 a 0.525,0.525 0 0 1 0.1288,0.7315 l -2.2085,3.1535 a 0.52500478,0.52500478 0 1 1 -0.8603,-0.602 l 2.2085,-3.1542 a 0.525,0.525 0 0 1 0.7315,-0.1288 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
190
client/src/lib/ckeditor.js
vendored
Normal file
190
client/src/lib/ckeditor.js
vendored
Normal file
|
@ -0,0 +1,190 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
|
||||
|
||||
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials';
|
||||
import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold';
|
||||
import UnderlinePlugin from '@ckeditor/ckeditor5-basic-styles/src/underline';
|
||||
import StrikethroughPlugin from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
|
||||
import CodePlugin from '@ckeditor/ckeditor5-basic-styles/src/code';
|
||||
import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic';
|
||||
import BlockQuotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote';
|
||||
import EasyImagePlugin from '@ckeditor/ckeditor5-easy-image/src/easyimage';
|
||||
import HeadingPlugin from '@ckeditor/ckeditor5-heading/src/heading';
|
||||
import ImagePlugin from '@ckeditor/ckeditor5-image/src/image';
|
||||
import ImageCaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption';
|
||||
import ImageStylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle';
|
||||
import ImageToolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar';
|
||||
import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload';
|
||||
import LinkPlugin from '@ckeditor/ckeditor5-link/src/link';
|
||||
import ListPlugin from '@ckeditor/ckeditor5-list/src/list';
|
||||
import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph';
|
||||
import AlignmentPlugin from '@ckeditor/ckeditor5-alignment/src/alignment';
|
||||
import TablePlugin from '@ckeditor/ckeditor5-table/src/table';
|
||||
import TableToolbarPlugin from '@ckeditor/ckeditor5-table/src/tabletoolbar';
|
||||
|
||||
import CKEditor from '@ckeditor/ckeditor5-react';
|
||||
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
|
||||
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
|
||||
|
||||
import insertImageIcon from './ckeditor-insert-image.svg';
|
||||
|
||||
class InsertImage extends Plugin {
|
||||
init() {
|
||||
const editor = this.editor;
|
||||
|
||||
editor.ui.componentFactory.add( 'insertImage', locale => {
|
||||
const view = new ButtonView( locale );
|
||||
|
||||
view.set( {
|
||||
label: 'Insert image',
|
||||
icon: insertImageIcon,
|
||||
tooltip: true
|
||||
} );
|
||||
|
||||
// Callback executed once the image is clicked.
|
||||
view.on( 'execute', () => {
|
||||
let url = '';
|
||||
const selectedElement = editor.model.document.selection.getSelectedElement();
|
||||
if (selectedElement) {
|
||||
if (selectedElement.is('element', 'image')) {
|
||||
url = selectedElement.getAttribute('src');
|
||||
}
|
||||
}
|
||||
|
||||
const imageUrl = prompt('Image URL', url);
|
||||
|
||||
if (imageUrl) {
|
||||
editor.model.change( writer => {
|
||||
const imageElement = writer.createElement( 'image', {
|
||||
src: imageUrl
|
||||
} );
|
||||
|
||||
// Insert the image in the current selection location.
|
||||
editor.model.insertContent( imageElement, editor.model.document.selection );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
return view;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UploadAdapter {
|
||||
constructor(loader, url, t) {
|
||||
this.loader = loader;
|
||||
}
|
||||
|
||||
async upload() {
|
||||
console.log(this.loader);
|
||||
return {
|
||||
default: 'http://server/default-size.image.png'
|
||||
};
|
||||
}
|
||||
|
||||
abort() {
|
||||
}
|
||||
}
|
||||
|
||||
class MailtrainUploadAdapter extends Plugin {
|
||||
static get requires() {
|
||||
return [ FileRepository ];
|
||||
}
|
||||
|
||||
static get pluginName() {
|
||||
return 'MailtrainUploadAdapter';
|
||||
}
|
||||
|
||||
init() {
|
||||
this.editor.plugins.get(FileRepository).createUploadAdapter = loader => new UploadAdapter(loader, this.editor.t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ClassicEditor extends ClassicEditorBase {}
|
||||
|
||||
ClassicEditor.builtinPlugins = [
|
||||
EssentialsPlugin,
|
||||
BoldPlugin,
|
||||
ItalicPlugin,
|
||||
UnderlinePlugin,
|
||||
StrikethroughPlugin,
|
||||
CodePlugin,
|
||||
BlockQuotePlugin,
|
||||
HeadingPlugin,
|
||||
ImagePlugin,
|
||||
ImageCaptionPlugin,
|
||||
ImageStylePlugin,
|
||||
ImageToolbarPlugin,
|
||||
ImageUploadPlugin,
|
||||
LinkPlugin,
|
||||
ListPlugin,
|
||||
ParagraphPlugin,
|
||||
AlignmentPlugin,
|
||||
TablePlugin,
|
||||
TableToolbarPlugin,
|
||||
MailtrainUploadAdapter,
|
||||
InsertImage
|
||||
];
|
||||
|
||||
ClassicEditor.defaultConfig = {
|
||||
toolbar: {
|
||||
items: [
|
||||
'heading',
|
||||
'|',
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strikethrough',
|
||||
'code',
|
||||
'|',
|
||||
'alignment',
|
||||
'|',
|
||||
'link',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'|',
|
||||
'insertImage',
|
||||
'imageUpload',
|
||||
'blockQuote',
|
||||
'|',
|
||||
'insertTable',
|
||||
'|',
|
||||
'undo',
|
||||
'redo'
|
||||
]
|
||||
},
|
||||
alignment: {
|
||||
options: [ 'left', 'center', 'right', 'justify' ]
|
||||
},
|
||||
image: {
|
||||
toolbar: [
|
||||
'imageStyle:full',
|
||||
'imageStyle:side',
|
||||
'|',
|
||||
'imageTextAlternative'
|
||||
]
|
||||
},
|
||||
table: {
|
||||
contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
|
||||
},
|
||||
language: 'en'
|
||||
};
|
||||
|
||||
export default class CKEditorWrapper extends Component {
|
||||
render() {
|
||||
return (
|
||||
<CKEditor
|
||||
editor={ ClassicEditor }
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -160,7 +160,7 @@ export default class Files extends Component {
|
|||
|
||||
{
|
||||
this.props.entity.permissions.includes(this.props.managePermission) &&
|
||||
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
||||
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName={styles.dropZoneActive}>
|
||||
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
||||
</Dropzone>
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import ACEEditorRaw from 'react-ace';
|
|||
import 'brace/theme/github';
|
||||
import 'brace/ext/searchbox';
|
||||
|
||||
import CKEditorRaw from "react-ckeditor-component";
|
||||
import CKEditorRaw from './ckeditor';
|
||||
|
||||
import DayPicker from 'react-day-picker';
|
||||
import 'react-day-picker/lib/style.css';
|
||||
|
@ -892,11 +892,11 @@ class CKEditor extends Component {
|
|||
|
||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||
<CKEditorRaw
|
||||
events={{
|
||||
"change": evt => owner.updateFormValue(id, evt.editor.getData())
|
||||
onChange={(event, editor) => owner.updateFormValue(id, editor.getData())}
|
||||
onInit={ editor => {
|
||||
editor.ui.view.editable.editableElement.style.height = props.height;
|
||||
} }
|
||||
content={owner.getFormValue(id)}
|
||||
config={{width: '100%', height: props.height}}
|
||||
data={owner.getFormValue(id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export class RestActionModalDialog extends Component {
|
|||
|
||||
async hideModal(isBack) {
|
||||
if (this.props.backUrl) {
|
||||
this.props.stateOwner.navigateTo(this.props.backUrl);
|
||||
this.navigateTo(this.props.backUrl);
|
||||
} else {
|
||||
if (isBack) {
|
||||
this.props.onBack();
|
||||
|
@ -68,7 +68,7 @@ export class RestActionModalDialog extends Component {
|
|||
await axios.method(props.actionMethod, getUrl(props.actionUrl));
|
||||
|
||||
if (props.successUrl) {
|
||||
owner.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
|
||||
this.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
|
||||
} else {
|
||||
props.onSuccess();
|
||||
this.setFlashMessage('success', props.actionDoneMsg);
|
||||
|
|
|
@ -345,7 +345,7 @@ class SectionContent extends Component {
|
|||
async closeFlashMessage() {
|
||||
this.setState({
|
||||
flashMessageText: ''
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
renderRoute(route) {
|
||||
|
@ -462,8 +462,12 @@ function requiresAuthenticatedUser(target) {
|
|||
const comp1 = withPageHelpers(target);
|
||||
|
||||
function comp2(props, context) {
|
||||
if (!new.target) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
context.sectionContent.ensureAuthenticated();
|
||||
comp1.call(this, props, context);
|
||||
return Reflect.construct(comp1, [props, context], new.target);
|
||||
}
|
||||
|
||||
comp2.prototype = comp1.prototype;
|
||||
|
|
23
client/src/lib/sandboxed-ckeditor-root.js
Normal file
23
client/src/lib/sandboxed-ckeditor-root.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
import './public-path';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {I18nextProvider,} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import {CKEditorSandbox} from './sandboxed-ckeditor';
|
||||
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
||||
|
||||
export default function() {
|
||||
parentRPC.init();
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={ i18n }>
|
||||
<UntrustedContentRoot render={props => <CKEditorSandbox {...props} />} />
|
||||
</I18nextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
|
110
client/src/lib/sandboxed-ckeditor.js
Normal file
110
client/src/lib/sandboxed-ckeditor.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {translate} from 'react-i18next';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./sandboxed-ckeditor.scss";
|
||||
|
||||
import {UntrustedContentHost, parentRPC} from './untrusted';
|
||||
import {Icon} from "./bootstrap-components";
|
||||
import {
|
||||
getPublicUrl,
|
||||
getSandboxUrl,
|
||||
getTrustedUrl
|
||||
} from "./urls";
|
||||
import {
|
||||
base,
|
||||
unbase
|
||||
} from "../../../shared/templates";
|
||||
import CKEditor from './ckeditor';
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
export class CKEditorHost extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
initialHtml: PropTypes.string
|
||||
}
|
||||
|
||||
async exportState() {
|
||||
return await this.contentNode.ask('exportState');
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const editorData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id,
|
||||
initialHtml: this.props.initialHtml
|
||||
};
|
||||
|
||||
const tokenData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.editor}>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="ckeditor/editor" tokenMethod="ckeditor" tokenParams={editorData}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CKEditorHost.prototype.exportState = async function() {
|
||||
return await this.getWrappedInstance().exportState();
|
||||
};
|
||||
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
export class CKEditorSandbox extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const trustedUrlBase = getTrustedUrl();
|
||||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
const html = this.props.initialHtml && base(this.props.initialHtml, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||
|
||||
this.state = {
|
||||
html
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entityId: PropTypes.number,
|
||||
initialHtml: PropTypes.string
|
||||
}
|
||||
|
||||
async exportState(method, params) {
|
||||
const trustedUrlBase = getTrustedUrl();
|
||||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
return {
|
||||
html: unbase(this.state.html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.sandbox}>
|
||||
<CKEditor
|
||||
onChange={(event, editor) => this.setState({html: editor.getData()})}
|
||||
data={this.state.html}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
10
client/src/lib/sandboxed-ckeditor.scss
Normal file
10
client/src/lib/sandboxed-ckeditor.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.editor {
|
||||
.host {
|
||||
}
|
||||
}
|
||||
|
||||
.sandbox {
|
||||
:global .ck-editor__editable {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import {I18nextProvider,} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import {MosaicoSandbox} from './mosaico';
|
||||
import {MosaicoSandbox} from './sandboxed-mosaico';
|
||||
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
||||
|
||||
export default function() {
|
|
@ -3,7 +3,7 @@
|
|||
import React, {Component} from 'react';
|
||||
import {translate} from 'react-i18next';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./mosaico.scss";
|
||||
import styles from "./sandboxed-mosaico.scss";
|
||||
|
||||
import {UntrustedContentHost, parentRPC} from './untrusted';
|
||||
import {Icon} from "./bootstrap-components";
|
||||
|
@ -18,13 +18,8 @@ import {
|
|||
} from "../../../shared/templates";
|
||||
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
export class MosaicoEditor extends Component {
|
||||
export class MosaicoEditorHost extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -59,7 +54,7 @@ export class MosaicoEditor extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const mosaicoData = {
|
||||
const editorData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id,
|
||||
templateId: this.props.templateId,
|
||||
|
@ -68,6 +63,11 @@ export class MosaicoEditor extends Component {
|
|||
initialMetadata: this.props.initialMetadata
|
||||
};
|
||||
|
||||
const tokenData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
|
||||
<div className={styles.navbar}>
|
||||
|
@ -75,13 +75,13 @@ export class MosaicoEditor extends Component {
|
|||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={tokenData}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MosaicoEditor.prototype.exportState = async function() {
|
||||
MosaicoEditorHost.prototype.exportState = async function() {
|
||||
return await this.getWrappedInstance().exportState();
|
||||
};
|
||||
|
|
@ -121,6 +121,12 @@
|
|||
color: #808080;
|
||||
}
|
||||
|
||||
.dropZoneActive{
|
||||
border-color: #90EE90;
|
||||
color: #000;
|
||||
background-color: #DDFFDD;
|
||||
}
|
||||
|
||||
|
||||
.untrustedContent {
|
||||
border: 0px none;
|
||||
|
|
|
@ -16,7 +16,6 @@ import { withPageHelpers } from './page'
|
|||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import styles from "./styles.scss";
|
||||
import {getUrl} from "./urls";
|
||||
import {Table} from "./table";
|
||||
|
||||
const TreeSelectMode = {
|
||||
NONE: 0,
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class List extends Component {
|
|||
const perms = data[9];
|
||||
const campaignId = data[8];
|
||||
|
||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && perms.includes('manageTriggers')) {
|
||||
if (mailtrainConfig.globalPermissions.setupAutomation && perms.includes('manageTriggers')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
||||
|
|
|
@ -66,7 +66,7 @@ export default class List extends Component {
|
|||
refreshTimeout = 1000;
|
||||
}
|
||||
|
||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports')) {
|
||||
if (mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
|
||||
|
@ -90,7 +90,7 @@ export default class List extends Component {
|
|||
return (
|
||||
<div>
|
||||
{tableDeleteDialogRender(this, `rest/imports/${this.props.list.id}`, t('Deleting import ...'), t('Import deleted'))}
|
||||
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
|
||||
{mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports') &&
|
||||
<Toolbar>
|
||||
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
|
||||
</Toolbar>
|
||||
|
|
|
@ -76,7 +76,7 @@ function getMenus(t) {
|
|||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/reports/templates/${params.templateId}/edit`,
|
||||
visible: resolved => mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && resolved.template.permissions.includes('edit'),
|
||||
visible: resolved => mailtrainConfig.globalPermissions.createJavascriptWithROAccess && resolved.template.permissions.includes('edit'),
|
||||
panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
||||
},
|
||||
share: {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class List extends Component {
|
|||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess')
|
||||
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.createJavascriptWithROAccess
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default class List extends Component {
|
|||
const actions = [];
|
||||
const perms = data[5];
|
||||
|
||||
if (mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && perms.includes('edit')) {
|
||||
if (mailtrainConfig.globalPermissions.createJavascriptWithROAccess && perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/reports/templates/${data[0]}/edit`
|
||||
|
|
|
@ -84,9 +84,9 @@ class Root extends Component {
|
|||
<DropdownMenuItem label={t('Administration')}>
|
||||
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
||||
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
||||
{mailtrainConfig.globalPermissions.includes('manageSettings') && <MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>}
|
||||
{mailtrainConfig.globalPermissions.manageSettings && <MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>}
|
||||
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
||||
{mailtrainConfig.globalPermissions.includes('manageBlacklist') && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>}
|
||||
{mailtrainConfig.globalPermissions.manageBlacklist && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>}
|
||||
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
||||
</DropdownMenuItem>
|
||||
</ul>
|
||||
|
|
|
@ -12,10 +12,8 @@ import {
|
|||
import 'brace/mode/text';
|
||||
import 'brace/mode/html';
|
||||
|
||||
import {
|
||||
MosaicoEditor,
|
||||
ResourceType
|
||||
} from "../lib/mosaico";
|
||||
import { MosaicoEditorHost } from "../lib/sandboxed-mosaico";
|
||||
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
|
||||
|
||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||
import {getSandboxUrl} from "../lib/urls";
|
||||
|
@ -28,6 +26,10 @@ import {Trans} from "react-i18next";
|
|||
|
||||
import styles from "../lib/styles.scss";
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE) {
|
||||
// The prefix is used to to enable use within other forms (i.e. campaign form)
|
||||
|
@ -67,7 +69,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
|
||||
getHTMLEditor: owner =>
|
||||
<AlignedRow label={t('Template content (HTML)')}>
|
||||
<MosaicoEditor
|
||||
<MosaicoEditorHost
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||
|
@ -129,7 +131,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
},
|
||||
getHTMLEditor: owner =>
|
||||
<AlignedRow label={t('Template content (HTML)')}>
|
||||
<MosaicoEditor
|
||||
<MosaicoEditorHost
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||
|
@ -172,7 +174,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.grapejs = { // TODO
|
||||
templateTypes.grapejs = { // FIXME
|
||||
typeName: t('GrapeJS'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => null,
|
||||
|
@ -186,10 +188,34 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.xckeditor = {
|
||||
typeName: t('CKEditor'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner =>
|
||||
<AlignedRow label={t('Template content (HTML)')}>
|
||||
<CKEditorHost
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialHtml={owner.getFormValue(prefix + 'html')}
|
||||
entityTypeId={entityTypeId}/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue(prefix + 'html', html);
|
||||
},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
beforeSave: data => {
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.ckeditor = {
|
||||
typeName: t('CKEditor'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" label={t('Template content (HTML)')}/>,
|
||||
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||
exportHTMLEditorData: async owner => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
|
@ -214,7 +240,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.mjml = { // TODO
|
||||
templateTypes.mjml = { // FIXME
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => null,
|
||||
exportHTMLEditorData: async owner => {},
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
// The CKEditor part comes from https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/advanced-setup.html
|
||||
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
|
||||
const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new CKEditorWebpackPlugin( {
|
||||
// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
|
||||
language: 'en'
|
||||
} )
|
||||
],
|
||||
entry: {
|
||||
root: ['babel-polyfill', './src/root.js'],
|
||||
mosaico: ['babel-polyfill', './src/lib/mosaico-sandbox-root.js'],
|
||||
mosaico: ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'],
|
||||
ckeditor: ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'],
|
||||
},
|
||||
output: {
|
||||
library: 'MailtrainReactBody',
|
||||
|
@ -15,12 +26,51 @@ module.exports = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
use: [ 'babel-loader' ]
|
||||
exclude: path.join(__dirname, 'node_modules'),
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['env', {
|
||||
targets: {
|
||||
"chrome": "58",
|
||||
"edge": "15",
|
||||
"firefox": "55",
|
||||
"ios": "10"
|
||||
}
|
||||
}],
|
||||
'stage-1'
|
||||
],
|
||||
plugins: ['transform-react-jsx', 'transform-decorators-legacy', 'transform-function-bind']
|
||||
}
|
||||
}
|
||||
]
|
||||
// exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
// use: [ 'babel-loader' ]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [ 'style-loader', 'css-loader' ]
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader',
|
||||
options: {
|
||||
singleton: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'css-loader'
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: styles.getPostCssConfig( {
|
||||
themeImporter: {
|
||||
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
|
||||
},
|
||||
minify: false
|
||||
} )
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
|
@ -33,6 +83,13 @@ module.exports = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Or /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg$/ if you want to limit this loader
|
||||
// to CKEditor 5 icons only.
|
||||
test: /\.svg$/,
|
||||
|
||||
use: [ 'raw-loader' ]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
exclude: path.join(__dirname, 'node_modules'),
|
||||
|
|
|
@ -5,7 +5,6 @@ const config = require('config');
|
|||
const forms = require('../models/forms');
|
||||
const shares = require('../models/shares');
|
||||
const urls = require('./urls');
|
||||
const { AppType } = require('../shared/app');
|
||||
|
||||
|
||||
async function getAnonymousConfig(context, appType) {
|
||||
|
@ -26,6 +25,11 @@ async function getAnonymousConfig(context, appType) {
|
|||
}
|
||||
|
||||
async function getAuthenticatedConfig(context) {
|
||||
const globalPermissions = {};
|
||||
for (const perm of shares.getGlobalPermissions(context)) {
|
||||
globalPermissions[perm] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
defaultCustomFormValues: await forms.getDefaultCustomFormValues(),
|
||||
user: {
|
||||
|
@ -33,7 +37,7 @@ async function getAuthenticatedConfig(context) {
|
|||
username: context.user.username,
|
||||
namespace: context.user.namespace
|
||||
},
|
||||
globalPermissions: shares.getGlobalPermissions(context),
|
||||
globalPermissions,
|
||||
editors: config.editors,
|
||||
mosaico: config.mosaico,
|
||||
verpEnabled: config.verp.enabled
|
||||
|
|
|
@ -55,4 +55,4 @@ async function ensureNoDependencies(tx, context, id, depSpecs) {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports.ensureNoDependencies = ensureNoDependencies
|
||||
module.exports.ensureNoDependencies = ensureNoDependencies;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
let _ = require('./translate')._;
|
||||
|
||||
module.exports = {
|
||||
enforce,
|
||||
cleanupFromPost,
|
||||
|
|
|
@ -24,7 +24,7 @@ async function listTree(context) {
|
|||
|
||||
// Build a tree
|
||||
const rows = await knex('namespaces')
|
||||
.innerJoin(entityType.permissionsTable, {
|
||||
.leftJoin(entityType.permissionsTable, {
|
||||
[entityType.permissionsTable + '.entity']: 'namespaces.id',
|
||||
[entityType.permissionsTable + '.user']: context.user.id
|
||||
})
|
||||
|
@ -63,7 +63,7 @@ async function listTree(context) {
|
|||
entry.key = row.id;
|
||||
entry.title = row.name;
|
||||
entry.description = row.description;
|
||||
entry.permissions = row.permissions.split(';');
|
||||
entry.permissions = row.permissions ? row.permissions.split(';') : [];
|
||||
}
|
||||
|
||||
// Prune out the inaccessible namespaces
|
||||
|
@ -160,7 +160,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
|
||||
if (iter.id == entity.id) {
|
||||
if (iter.id === entity.id) {
|
||||
throw new interoperableErrors.LoopDetectedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ const { enforce } = require('../lib/helpers');
|
|||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const entitySettings = require('../lib/entity-settings');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const log = require('npmlog');
|
||||
const log = require('../lib/log');
|
||||
const {getGlobalNamespaceId} = require('../shared/namespaces');
|
||||
const {getAdminId} = require('../shared/users');
|
||||
|
||||
|
|
61
routes/sandboxed-ckeditor.js
Normal file
61
routes/sandboxed-ckeditor.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
|
||||
const routerFactory = require('../lib/router-async');
|
||||
const passport = require('../lib/passport');
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
const users = require('../models/users');
|
||||
|
||||
const files = require('../models/files');
|
||||
const fileHelpers = require('../lib/file-helpers');
|
||||
|
||||
const templates = require('../models/templates');
|
||||
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
|
||||
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
|
||||
const { AppType } = require('../shared/app');
|
||||
|
||||
|
||||
users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, entityId}) => {
|
||||
if (entityTypeId === 'template') {
|
||||
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
|
||||
|
||||
if (tmpl.type === 'ckeditor') {
|
||||
return {
|
||||
permissions: {
|
||||
'template': {
|
||||
[entityId]: new Set(['manageFiles', 'view'])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function getRouter(appType) {
|
||||
const router = routerFactory.create();
|
||||
|
||||
if (appType === AppType.SANDBOXED) {
|
||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
||||
|
||||
res.render('ckeditor/root', {
|
||||
layout: 'ckeditor/layout',
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig),
|
||||
scriptFiles: [
|
||||
getSandboxUrl('mailtrain/common.js'),
|
||||
getSandboxUrl('mailtrain/ckeditor.js')
|
||||
],
|
||||
publicPath: getSandboxUrl()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports.getRouter = getRouter;
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const routerFactory = require('../lib/router-async');
|
||||
const passport = require('../lib/passport');
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
@ -201,7 +201,7 @@ function getRouter(appType) {
|
|||
getSandboxUrl('mailtrain/common.js'),
|
||||
getSandboxUrl('mailtrain/mosaico.js')
|
||||
],
|
||||
mosaicoPublicPath: getSandboxUrl('static/mosaico')
|
||||
publicPath: getSandboxUrl()
|
||||
});
|
||||
});
|
||||
|
|
@ -136,7 +136,6 @@ const errorTypes = {
|
|||
|
||||
function deserialize(errorObj) {
|
||||
if (errorObj.type) {
|
||||
|
||||
const ctor = errorTypes[errorObj.type];
|
||||
if (ctor) {
|
||||
return new ctor(errorObj.message, errorObj.data);
|
||||
|
|
7532
shared/package-lock.json
generated
7532
shared/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -16,5 +16,21 @@
|
|||
"moment": "^2.18.1",
|
||||
"moment-timezone": "^0.5.13",
|
||||
"owasp-password-strength-test": "github:bures/owasp-password-strength-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-1": "^6.24.1",
|
||||
"css-loader": "^0.28.4",
|
||||
"i18next-conv": "^3.0.3",
|
||||
"node-sass": "^4.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"url-loader": "^0.5.9",
|
||||
"webpack": "^2.6.1"
|
||||
}
|
||||
}
|
||||
|
|
37
views/ckeditor/layout.hbs
Normal file
37
views/ckeditor/layout.hbs
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
|
||||
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{publicPath}}static/favicon.ico">
|
||||
|
||||
<title>Mailtrain
|
||||
{{#if title}} | {{title}}{{/if}}
|
||||
</title>
|
||||
|
||||
<link rel="stylesheet" href="{{publicPath}}static/bootstrap/themes/united.min.css">
|
||||
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
|
||||
<script src="{{publicPath}}static/bootstrap/js/bootstrap.min.js"></script>
|
||||
|
||||
{{#if mailtrainConfig}}
|
||||
<script>
|
||||
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
|
||||
window.mailtrainConfig = {{{mailtrainConfig}}};
|
||||
</script>
|
||||
|
||||
{{#each scriptFiles}}
|
||||
<script src="{{this}}"></script>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{{body}}}
|
||||
</body>
|
||||
|
||||
</html>
|
6
views/ckeditor/root.hbs
Normal file
6
views/ckeditor/root.hbs
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div id="root"></div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
MailtrainReactBody.default();
|
||||
});
|
||||
</script>
|
|
@ -7,6 +7,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
|
||||
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="/static/favicon.ico">
|
||||
|
||||
<title>Mailtrain
|
||||
|
|
|
@ -4,27 +4,27 @@
|
|||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=1024, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||
|
||||
<title>Mailtrain</title>
|
||||
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery-migrate.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/knockout.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery-ui.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.ui.touch-punch.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/load-image.all.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/canvas-to-blob.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.iframe-transport.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-process.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-image.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-validate.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/knockout-jqueryui.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/tinymce.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery-migrate.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/knockout.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery-ui.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.ui.touch-punch.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/load-image.all.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/canvas-to-blob.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.iframe-transport.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload-process.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload-image.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload-validate.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/knockout-jqueryui.min.js"></script>
|
||||
<script src="{{publicPath}}static/mosaico/vendor/tinymce.min.js"></script>
|
||||
|
||||
<script src="{{mosaicoPublicPath}}/mosaico.min.js?v=0.16"></script>
|
||||
<script src="{{publicPath}}static/mosaico/mosaico.min.js?v=0.16"></script>
|
||||
|
||||
{{#if languageStrings}}<script> window.mosaicoLanguageStrings = {{{languageStrings}}}; </script>{{/if}}
|
||||
<script> window.mosaicoPlugins = []; </script>
|
||||
|
@ -45,13 +45,11 @@
|
|||
<script src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
<link rel="stylesheet" href="{{mosaicoPublicPath}}/mosaico-material.min.css?v=0.10" />
|
||||
<link rel="stylesheet" href="{{mosaicoPublicPath}}/vendor/notoregular/stylesheet.css" />
|
||||
<link rel="stylesheet" href="{{publicPath}}static/mosaico/mosaico-material.min.css?v=0.10" />
|
||||
<link rel="stylesheet" href="{{publicPath}}static/mosaico/vendor/notoregular/stylesheet.css" />
|
||||
</head>
|
||||
<body class="mo-standalone">
|
||||
{{{body}}}
|
||||
|
||||
{{> tracking_scripts}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue