Fixed sandbox. Multiple tabs work now.

WiP on selectable mosaico templates.

TODO: Make files always point to trusted URL, such that we don't have to rebase them. They are public anyway. The same goes for mosaico endpoints: /mosaico/templates and /mosaico/img
This commit is contained in:
Tomas Bures 2018-05-09 04:07:01 +02:00
parent a4ee1534cc
commit 7788b0bc67
79 changed files with 724 additions and 390 deletions

View file

@ -153,10 +153,6 @@ function createApp(trusted) {
}));
app.use(cookieParser());
useWith404Fallback('/public', express.static(path.join(__dirname, 'client', 'public')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));
app.use(session({
store: config.redis.enabled ? new RedisStore(config.redis) : false,
secret: config.www.secret,
@ -185,6 +181,10 @@ function createApp(trusted) {
app.use(passport.tryAuthByRestrictedAccessToken);
}
useWith404Fallback('/public', express.static(path.join(__dirname, 'client', 'public')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));
/* FIXME - can we remove this???
// make sure flash messages are available

View file

@ -24,7 +24,7 @@ export default class API extends Component {
@withAsyncErrorHandler
async loadAccessToken() {
const response = await axios.get('/rest/access-token');
const response = await axios.get(getUrl('rest/access-token'));
this.setState({
accessToken: response.data
});
@ -35,7 +35,7 @@ export default class API extends Component {
}
async resetAccessToken() {
const response = await axios.post('/rest/access-token-reset');
const response = await axios.post(getUrl('rest/access-token-reset'));
this.setState({
accessToken: response.data
});

View file

@ -26,7 +26,7 @@ export default class Account extends Component {
this.initForm({
serverValidation: {
url: '/rest/account-validate',
url: 'rest/account-validate',
changed: ['email', 'currentPassword']
}
});
@ -34,7 +34,7 @@ export default class Account extends Component {
@withAsyncErrorHandler
async loadFormValues() {
await this.getFormValuesFromURL('/rest/account', data => {
await this.getFormValuesFromURL('rest/account', data => {
data.password = '';
data.password2 = '';
data.currentPassword = '';
@ -113,7 +113,7 @@ export default class Account extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Updating user profile ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/account', data => {
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/account', data => {
delete data.password2;
});

View file

@ -44,7 +44,7 @@ export default class Forget extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Processing ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/password-reset-send');
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/password-reset-send');
if (submitSuccessful) {
this.navigateToWithFlashMessage('/account/login', 'success', t('If the username / email exists in the system, password reset link will be sent to the registered email.'));

View file

@ -59,7 +59,7 @@ export default class Login extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Verifying credentials ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/login');
if (submitSuccessful) {
const nextUrl = qs.parse(this.props.location.search).next || getUrl();

View file

@ -11,6 +11,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
import passwordValidator from '../../../shared/password-validator';
import axios from '../lib/axios';
import interoperableErrors from '../../../shared/interoperable-errors';
import {getUrl} from "../lib/urls";
const ResetTokenValidationState = {
PENDING: 0,
@ -39,7 +40,7 @@ export default class Account extends Component {
async validateResetToken() {
const params = this.props.match.params;
const response = await axios.post('/rest/password-reset-validate', {
const response = await axios.post(getUrl('rest/password-reset-validate'), {
username: params.username,
resetToken: params.resetToken
});
@ -90,7 +91,7 @@ export default class Account extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Resetting password ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/password-reset', data => {
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/password-reset', data => {
delete data.password2;
});

View file

@ -8,6 +8,7 @@ import {Table} from "../lib/table";
import {ButtonRow, Form, InputField, withForm, FormSendMethod} from "../lib/form";
import {Button, Icon} from "../lib/bootstrap-components";
import axios from "../lib/axios";
import {getUrl} from "../lib/urls";
@translate()
@withForm
@ -24,7 +25,7 @@ export default class List extends Component {
this.initForm({
serverValidation: {
url: '/rest/blacklist-validate',
url: 'rest/blacklist-validate',
changed: ['email']
}
});
@ -64,7 +65,7 @@ export default class List extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/blacklist');
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/blacklist');
if (submitSuccessful) {
this.hideFormValidation();
@ -86,7 +87,7 @@ export default class List extends Component {
@withAsyncErrorHandler
async deleteBlacklisted(email) {
await axios.delete(`/rest/blacklist/${email}`);
await axios.delete(getUrl(`rest/blacklist/${email}`));
this.blacklistTable.refresh();
}
@ -122,7 +123,7 @@ export default class List extends Component {
<h3 className="legend">{t('Blacklisted Emails')}</h3>
<Table ref={node => this.blacklistTable = node} withHeader dataUrl="/rest/blacklist-table" columns={columns} />
<Table ref={node => this.blacklistTable = node} withHeader dataUrl="rest/blacklist-table" columns={columns} />
</div>
);
}

View file

@ -9,10 +9,10 @@ const axiosInst = axios.create({
});
const axiosWrapper = {
get: (...args) => axiosInst.get(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
put: (...args) => axiosInst.put(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
post: (...args) => axiosInst.post(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
delete: (...args) => axiosInst.delete(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error })
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
};
const HTTPMethod = {

View file

@ -12,6 +12,7 @@ import {Icon} from "./bootstrap-components";
import axios from './axios';
import styles from "./styles.scss";
import {withPageHelpers} from "./page";
import {getUrl} from "./urls";
@translate()
@withErrorHandling
@ -64,7 +65,7 @@ export default class Files extends Component {
for (const file of files) {
data.append('files[]', file)
}
axios.post(`/rest/files/${this.props.entityTypeId}/${this.props.entity.id}`, data)
axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entity.id}`), data)
.then(res => {
this.filesTable.refresh();
const message = this.getFilesUploadedMessage(res);
@ -92,7 +93,7 @@ export default class Files extends Component {
try {
this.setFlashMessage('info', t('Deleting file ...'));
await axios.delete(`/rest/files/${this.props.entityTypeId}/${fileToDeleteId}`);
await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${fileToDeleteId}`));
this.filesTable.refresh();
this.setFlashMessage('info', t('File deleted'));
} catch (err) {
@ -114,7 +115,7 @@ export default class Files extends Component {
if (this.props.usePublicDownloadUrls) {
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
} else {
downloadUrl =`/rest/files/${this.props.entityTypeId}/${data[0]}`;
downloadUrl =`rest/files/${this.props.entityTypeId}/${data[0]}`;
}
const actions = [
@ -148,7 +149,7 @@ export default class Files extends Component {
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
</Dropzone>
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
<Table withHeader ref={node => this.filesTable = node} dataUrl={`rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
</div>
);
}

View file

@ -15,6 +15,7 @@ import {Button, Icon} from "./bootstrap-components";
import brace from 'brace';
import ACEEditorRaw from 'react-ace';
import 'brace/theme/github';
import 'brace/ext/searchbox';
import CKEditorRaw from "react-ckeditor-component";
@ -24,6 +25,7 @@ import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birth
import styles from "./styles.scss";
import moment from "moment";
import {getUrl} from "./urls";
const FormState = {
Loading: 0,
@ -711,7 +713,8 @@ class TableSelect extends Component {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string
format: PropTypes.string,
disabled: PropTypes.bool
}
static defaultProps = {
@ -772,11 +775,13 @@ class TableSelect extends Component {
if (props.dropdown) {
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<div>
<div className={`input-group ${styles.tableSelectDropdown}`}>
<input type="text" className="form-control" value={this.state.selectedLabel} readOnly onClick={::this.toggleOpen}/>
<div className={(props.disabled ? '' : 'input-group ') + styles.tableSelectDropdown}>
<input type="text" className="form-control" value={this.state.selectedLabel} onClick={::this.toggleOpen} readOnly={!props.disabled} disabled={props.disabled}/>
{!props.disabled &&
<span className="input-group-btn">
<Button label={t('Select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
</span>
}
</div>
<div className={styles.tableSelectTable + (this.state.open ? '' : ' ' + styles.tableSelectTableHidden)}>
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
@ -926,7 +931,7 @@ function withForm(target) {
if (payloadNotEmpty) {
mutState.set('isServerValidationRunning', true);
axios.post(settings.serverValidation.url, payload)
axios.post(getUrl(settings.serverValidation.url), payload)
.then(response => {
self.setState(previousState => ({
@ -1011,7 +1016,7 @@ function withForm(target) {
});
}, 500);
const response = await axios.get(url);
const response = await axios.get(getUrl(url));
const data = response.data;
@ -1037,7 +1042,7 @@ function withForm(target) {
mutator(data);
}
const response = await axios.method(method, url, data);
const response = await axios.method(method, getUrl(url), data);
return response.data || true;

View file

@ -2,6 +2,7 @@ import i18n from 'i18next';
import XHR from 'i18next-xhr-backend';
// import Cache from 'i18next-localstorage-cache';
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "./urls";
i18n
.use(XHR)
@ -23,6 +24,10 @@ i18n
interpolation: {
escapeValue: false // not needed for react
},
backend: {
loadPath: getUrl('locales/{{lng}}/{{ns}}.json')
}
});

View file

@ -5,6 +5,7 @@ import axios, { HTTPMethod } from './axios';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import {ModalDialog} from "./bootstrap-components";
import {getUrl} from "./urls";
@translate()
class RestActionModalDialog extends Component {
@ -35,7 +36,7 @@ class RestActionModalDialog extends Component {
try {
owner.disableForm();
owner.setFormStatusMessage('info', this.props.actionInProgressMsg);
await axios.method(this.props.actionMethod, this.props.actionUrl);
await axios.method(this.props.actionMethod, getUrl(this.props.actionUrl));
owner.navigateToWithFlashMessage(this.props.successUrl, 'success', this.props.actionDoneMsg);
} catch (err) {

View file

@ -1,14 +1,13 @@
'use strict';
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import {
I18nextProvider,
} from 'react-i18next';
import {I18nextProvider,} from 'react-i18next';
import i18n from './i18n';
import styles from "./mosaico.scss";
import { MosaicoSandbox } from './mosaico';
import { UntrustedContentRoot } from './untrusted';
import {MosaicoSandbox} from './mosaico';
import {UntrustedContentRoot} from './untrusted';
export default function() {
ReactDOM.render(

View file

@ -1,21 +1,15 @@
'use strict';
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {
I18nextProvider,
translate
} from 'react-i18next';
import i18n from './i18n';
import {translate} from 'react-i18next';
import PropTypes from "prop-types";
import styles from "./mosaico.scss";
import mailtrainConfig from 'mailtrainConfig';
import { UntrustedContentHost } from './untrusted';
import {
Button,
Icon
} from "./bootstrap-components";
import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {getUrl} from "./urls";
import {base, unbase} from "../../../shared/templates";
export const ResourceType = {
TEMPLATE: 'template',
@ -36,7 +30,10 @@ export class MosaicoEditor extends Component {
entityTypeId: PropTypes.string,
entity: PropTypes.object,
title: PropTypes.string,
onFullscreenAsync: PropTypes.func
onFullscreenAsync: PropTypes.func,
templateId: PropTypes.number,
initialModel: PropTypes.string,
initialMetadata: PropTypes.string
}
async toggleFullscreenAsync() {
@ -57,18 +54,19 @@ export class MosaicoEditor extends Component {
const mosaicoData = {
entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id,
model: this.props.entity.data.model,
metadata: this.props.entity.data.metadata
templateId: this.props.templateId,
initialModel: this.props.initialModel,
initialMetadata: this.props.initialMetadata
};
return (
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
<div className={styles.navbar}>
{this.state.fullscreen && <img className={styles.logo} src="/public/mailtrain-notext.png"/>}
{this.state.fullscreen && <img className={styles.logo} src={getUrl('public/mailtrain-notext.png')}/>}
<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} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
</div>
);
}
@ -92,12 +90,13 @@ export class MosaicoSandbox extends Component {
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
model: PropTypes.object,
metadata: PropTypes.object
templateId: PropTypes.number,
initialModel: PropTypes.string,
initialMetadata: PropTypes.string
}
componentDidMount() {
const publicPath = '/public/mosaico';
const publicPath = 'public/mosaico';
if (!Mosaico.isCompatible()) {
alert('Update your browser!');
@ -124,22 +123,23 @@ export class MosaicoSandbox extends Component {
plugins.unshift(vm => {
// This is an override of the default paths in Mosaico
vm.logoPath = publicPath + '/img/mosaico32.png';
vm.logoPath = getUrl(publicPath + '/img/mosaico32.png');
vm.logoUrl = '#';
});
const config = {
imgProcessorBackend: `/mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`,
emailProcessorBackend: '/mosaico/dl/',
imgProcessorBackend: getUrl(`mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`),
emailProcessorBackend: getUrl('mosaico/dl/'),
fileuploadConfig: {
url: `/mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`
url: getUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
},
strings: window.mosaicoLanguageStrings
};
const metadata = this.props.metadata;
const model = this.props.model;
const template = publicPath + '/templates/versafix-1/index.html';
const urlBase = getUrl();
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, urlBase));
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, urlBase));
const template = getUrl(`mosaico/templates/${this.props.templateId}/index.html`);
const allPlugins = plugins.concat(window.mosaicoPlugins);
@ -148,10 +148,11 @@ export class MosaicoSandbox extends Component {
async onMethodAsync(method, params) {
if (method === 'exportState') {
const urlBase = getUrl();
return {
html: this.viewModel.exportHTML(),
model: this.viewModel.exportJS(),
metadata: this.viewModel.metadata
html: unbase(this.viewModel.exportHTML(), urlBase),
model: unbase(this.viewModel.exportJSON(), urlBase),
metadata: unbase(this.viewModel.exportMetadata(), urlBase)
};
}
}

View file

@ -11,7 +11,7 @@ class NamespaceSelect extends Component {
const t = this.props.t;
return (
<TreeTableSelect id="namespace" label={t('Namespace')} dataUrl="/rest/namespaces-tree"/>
<TreeTableSelect id="namespace" label={t('Namespace')} dataUrl="rest/namespaces-tree"/>
);
}
}

View file

@ -5,6 +5,7 @@ import PropTypes from "prop-types";
import {withRouter} from "react-router";
import {withErrorHandling} from "./error-handling";
import axios from "../lib/axios";
import {getUrl} from "./urls";
function needsResolve(route, nextRoute, match, nextMatch) {
const resolve = route.resolve;
@ -30,7 +31,7 @@ async function resolve(route, match) {
const promises = keys.map(key => {
const url = route.resolve[key](match.params);
if (url) {
return axios.get(url);
return axios.get(getUrl(url));
} else {
return Promise.resolve({data: null});
}

View file

@ -0,0 +1,12 @@
'use strict';
import {getUrl} from "./urls";
import axios from "./axios";
async function checkPermissions(request) {
return await axios.post(getUrl('rest/permissions-check'), request);
}
export {
checkPermissions
}

View file

@ -0,0 +1,5 @@
'use strict';
import {getUrl} from "./urls";
__webpack_public_path__ = getUrl();

View file

@ -42,6 +42,12 @@
opacity: 1;
}
:global .form-control[disabled] {
cursor: default;
background-color: #eeeeee;
opacity: 1;
}
:global .ace_editor {
border: 1px solid #ccc;
}

View file

@ -16,6 +16,7 @@ import axios from './axios';
import { withPageHelpers } from './page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import styles from "./styles.scss";
import {getUrl} from "./urls";
//dtFactory();
//dtSelectFactory();
@ -122,7 +123,7 @@ class Table extends Component {
@withAsyncErrorHandler
async fetchData(data, callback) {
// This custom ajax fetch function allows us to properly handle the case when the user is not authenticated.
const response = await axios.post(this.props.dataUrl, data);
const response = await axios.post(getUrl(this.props.dataUrl), data);
callback(response.data);
}
@ -138,7 +139,7 @@ class Table extends Component {
}
if (keysToFetch.length > 0) {
const response = await axios.post(this.props.dataUrl, {
const response = await axios.post(getUrl(this.props.dataUrl), {
operation: 'getBy',
column: this.props.selectionKeyIndex,
values: keysToFetch

View file

@ -15,6 +15,7 @@ import axios from './axios';
import { withPageHelpers } from './page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import styles from "./styles.scss";
import {getUrl} from "./urls";
const TreeSelectMode = {
NONE: 0,
@ -47,7 +48,7 @@ class TreeTable extends Component {
@withAsyncErrorHandler
async loadData(dataUrl) {
const response = await axios.get(dataUrl);
const response = await axios.get(getUrl(dataUrl));
const treeData = response.data;
for (const root of treeData) {

View file

@ -3,12 +3,22 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import {requiresAuthenticatedUser, withPageHelpers} from "./page";
import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
import {
requiresAuthenticatedUser,
withPageHelpers
} from "./page";
import {
withAsyncErrorHandler,
withErrorHandling
} from "./error-handling";
import axios from "./axios";
import styles from "./styles.scss";
import {getTrustedUrl, getSandboxUrl} from "./urls";
import {Table} from "./table";
import {
getSandboxUrl,
getTrustedUrl,
getUrl,
setRestrictedAccessToken
} from "./urls";
@translate(null, { withRef: true })
@withPageHelpers
@ -37,7 +47,8 @@ export class UntrustedContentHost extends Component {
contentProps: PropTypes.object,
tokenMethod: PropTypes.string,
tokenParams: PropTypes.object,
className: PropTypes.string
className: PropTypes.string,
singleToken: PropTypes.bool
}
isInitialized() {
@ -73,6 +84,7 @@ export class UntrustedContentHost extends Component {
const msgId = this.rpcCounter;
this.sendMessage('rpcRequest', {
method,
params,
msgId
});
@ -85,20 +97,26 @@ export class UntrustedContentHost extends Component {
@withAsyncErrorHandler
async refreshAccessToken() {
const result = await axios.post(getTrustedUrl('rest/restricted-access-token'), {
method: this.props.tokenMethod,
params: this.props.tokenParams
});
if (this.props.singleToken && this.accessToken) {
await axios.put(getUrl('rest/restricted-access-token'), {
token: this.accessToken
});
} else {
const result = await axios.post(getUrl('rest/restricted-access-token'), {
method: this.props.tokenMethod,
params: this.props.tokenParams
});
this.accessToken = result.data;
this.accessToken = result.data;
if (!this.state.hasAccessToken) {
this.setState({
hasAccessToken: true
})
if (!this.state.hasAccessToken) {
this.setState({
hasAccessToken: true
})
}
this.sendMessage('accessToken', this.accessToken);
}
this.sendMessage('accessToken', this.accessToken);
}
scheduleRefreshAccessToken() {
@ -169,11 +187,6 @@ export class UntrustedContentRoot extends Component {
}
setAccessTokenCookie(token) {
document.cookie = 'restricted_access_token=' + token + '; expires=' + (new Date(Date.now()+60000)).toUTCString();
console.log(document.cookie);
}
async receiveMessage(evt) {
const msg = evt.data;
console.log(msg);
@ -182,14 +195,14 @@ export class UntrustedContentRoot extends Component {
this.sendMessage('initNeeded');
} else if (msg.type === 'init' && !this.state.initialized) {
this.setAccessTokenCookie(msg.data.accessToken);
setRestrictedAccessToken(msg.data.accessToken);
this.setState({
initialized: true,
contentProps: msg.data.contentProps
});
} else if (msg.type === 'accessToken') {
this.setAccessTokenCookie(msg.data);
setRestrictedAccessToken(msg.data);
} else if (msg.type === 'rpcRequest') {
const ret = await this.contentNode.onMethodAsync(msg.data.method, msg.data.params);
this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});

View file

@ -2,12 +2,18 @@
import mailtrainConfig from "mailtrainConfig";
let restrictedAccessToken = 'ANONYMOUS';
function setRestrictedAccessToken(token) {
restrictedAccessToken = token;
}
function getTrustedUrl(path) {
return mailtrainConfig.trustedUrlBase + (path || '');
}
function getSandboxUrl(path) {
return mailtrainConfig.sandboxUrlBase + (path || '');
return mailtrainConfig.sandboxUrlBase + restrictedAccessToken + '/' + (path || '');
}
function getUrl(path) {
@ -22,7 +28,7 @@ function getBaseDir() {
if (mailtrainConfig.trusted) {
return mailtrainConfig.trustedUrlBaseDir;
} else {
return mailtrainConfig.sandboxUrlBaseDir;
return mailtrainConfig.sandboxUrlBaseDir + 'ANONYMOUS';
}
}
@ -30,5 +36,6 @@ export {
getTrustedUrl,
getSandboxUrl,
getUrl,
getBaseDir
getBaseDir,
setRestrictedAccessToken
}

View file

@ -81,10 +81,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/lists/${this.props.entity.id}`
url = `rest/lists/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/lists'
url = 'rest/lists'
}
this.disableForm();
@ -164,7 +164,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/lists/${this.props.entity.id}`}
deleteUrl={`rest/lists/${this.props.entity.id}`}
cudUrl={`/lists/${this.props.entity.id}/edit`}
listUrl="/lists"
deletingMsg={t('Deleting list ...')}
@ -186,14 +186,14 @@ export default class CUD extends Component {
<InputField id="contact_email" label={t('Contact email')} help={t('Contact email used in subscription forms and emails that are sent out. If not filled in, the admin email from the global settings will be used.')}/>
<InputField id="homepage" label={t('Homepage')} help={t('Homepage URL used in subscription forms and emails that are sent out. If not filled in, the default homepage from global settings will be used.')}/>
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='/rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('Send configuration that will be used for sending out subscription-related emails.')}/>
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('Send configuration that will be used for sending out subscription-related emails.')}/>
<NamespaceSelect/>
<Dropdown id="form" label={t('Forms')} options={formsOptions} help={t('Web and email forms and templates used in subscription management process.')}/>
{this.getFormValue('form') === 'custom' &&
<TableSelect id="default_form" label={t('Custom forms')} withHeader dropdown dataUrl='/rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
<TableSelect id="default_form" label={t('Custom forms')} withHeader dropdown dataUrl='rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
}
<CheckBox id="public_subscribe" label={t('Subscription')} text={t('Allow public users to subscribe themselves')}/>

View file

@ -8,6 +8,7 @@ import { Table } from '../lib/table';
import axios from '../lib/axios';
import {Link} from "react-router-dom";
import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
@translate()
@withPageHelpers
@ -22,14 +23,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createList: {
entityTypeId: 'namespace',
requiredOperations: ['createList']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createList
@ -109,7 +108,7 @@ export default class List extends Component {
<Title>{t('Lists')}</Title>
<Table withHeader dataUrl="/rest/lists-table" columns={columns} />
<Table withHeader dataUrl="rest/lists-table" columns={columns} />
</div>
);
}

View file

@ -34,7 +34,7 @@ export default class CUD extends Component {
this.initForm({
serverValidation: {
url: `/rest/fields-validate/${this.props.list.id}`,
url: `rest/fields-validate/${this.props.list.id}`,
changed: ['key'],
extra: ['id']
},
@ -221,10 +221,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
url = `rest/fields/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `/rest/fields/${this.props.list.id}`
url = `rest/fields/${this.props.list.id}`
}
try {
@ -411,7 +411,7 @@ export default class CUD extends Component {
fieldSettings =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
@ -424,7 +424,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
deleteUrl={`rest/fields/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`}
listUrl={`/lists/${this.props.list.id}/fields`}
deletingMsg={t('Deleting field ...')}

View file

@ -55,7 +55,7 @@ export default class List extends Component {
<Title>{t('Fields')}</Title>
<Table withHeader dataUrl={`/rest/fields-table/${this.props.list.id}`} columns={columns} />
<Table withHeader dataUrl={`rest/fields-table/${this.props.list.id}`} columns={columns} />
</div>
);
}

View file

@ -52,7 +52,7 @@ export default class CUD extends Component {
this.initForm({
serverValidation: {
url: '/rest/forms-validate',
url: 'rest/forms-validate',
changed: this.serverValidatedFields
}
});
@ -316,10 +316,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/forms/${this.props.entity.id}`
url = `rest/forms/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/forms'
url = 'rest/forms'
}
this.disableForm();
@ -374,7 +374,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/forms/${this.props.entity.id}`}
deleteUrl={`rest/forms/${this.props.entity.id}`}
cudUrl={`/lists/forms/${this.props.entity.id}/edit`}
listUrl="/lists/forms"
deletingMsg={t('Deleting form ...')}
@ -391,7 +391,7 @@ export default class CUD extends Component {
<NamespaceSelect/>
<Fieldset label={t('Forms Preview')}>
<TableSelect id="previewList" label={t('List To Preview On')} withHeader dropdown dataUrl='/rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('Select list whose fields will be used to preview the forms.')}/>
<TableSelect id="previewList" label={t('List To Preview On')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('Select list whose fields will be used to preview the forms.')}/>
{ previewListId &&
<AlignedRow>

View file

@ -7,6 +7,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handli
import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import {Icon} from "../../lib/bootstrap-components";
import {checkPermissions} from "../../lib/permissions";
@translate()
@withPageHelpers
@ -21,14 +22,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createCustomForm: {
entityTypeId: 'namespace',
requiredOperations: ['createCustomForm']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createCustomForm
@ -79,7 +78,7 @@ export default class List extends Component {
<Title>{t('Forms')}</Title>
<Table withHeader dataUrl="/rest/forms-table" columns={columns} />
<Table withHeader dataUrl="rest/forms-table" columns={columns} />
</div>
);
}

View file

@ -25,14 +25,14 @@ function getMenus(t) {
':listId([0-9]+)': {
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
resolve: {
list: params => `/rest/lists/${params.listId}`
list: params => `rest/lists/${params.listId}`
},
link: params => `/lists/${params.listId}/subscriptions`,
navs: {
subscriptions: {
title: t('Subscribers'),
resolve: {
segments: params => `/rest/segments/${params.listId}`,
segments: params => `rest/segments/${params.listId}`,
},
link: params => `/lists/${params.listId}/subscriptions`,
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
@ -41,8 +41,8 @@ function getMenus(t) {
':subscriptionId([0-9]+)': {
title: resolved => resolved.subscription.email,
resolve: {
subscription: params => `/rest/subscriptions/${params.listId}/${params.subscriptionId}`,
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
subscription: params => `rest/subscriptions/${params.listId}/${params.subscriptionId}`,
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
},
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
navs: {
@ -56,7 +56,7 @@ function getMenus(t) {
create: {
title: t('Create'),
resolve: {
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
},
panelRender: props => <SubscriptionsCUD action="create" list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
}
@ -76,8 +76,8 @@ function getMenus(t) {
':fieldId([0-9]+)': {
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
resolve: {
field: params => `/rest/fields/${params.listId}/${params.fieldId}`,
fields: params => `/rest/fields/${params.listId}`
field: params => `rest/fields/${params.listId}/${params.fieldId}`,
fields: params => `rest/fields/${params.listId}`
},
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
navs: {
@ -91,7 +91,7 @@ function getMenus(t) {
create: {
title: t('Create'),
resolve: {
fields: params => `/rest/fields/${params.listId}`
fields: params => `rest/fields/${params.listId}`
},
panelRender: props => <FieldsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
}
@ -106,8 +106,8 @@ function getMenus(t) {
':segmentId([0-9]+)': {
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
resolve: {
segment: params => `/rest/segments/${params.listId}/${params.segmentId}`,
fields: params => `/rest/fields/${params.listId}`
segment: params => `rest/segments/${params.listId}/${params.segmentId}`,
fields: params => `rest/fields/${params.listId}`
},
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
navs: {
@ -121,7 +121,7 @@ function getMenus(t) {
create: {
title: t('Create'),
resolve: {
fields: params => `/rest/fields/${params.listId}`
fields: params => `rest/fields/${params.listId}`
},
panelRender: props => <SegmentsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
}
@ -147,7 +147,7 @@ function getMenus(t) {
':formsId([0-9]+)': {
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
resolve: {
forms: params => `/rest/forms/${params.formsId}`
forms: params => `rest/forms/${params.formsId}`
},
link: params => `/lists/forms/${params.formsId}/edit`,
navs: {

View file

@ -151,10 +151,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/segments/${this.props.list.id}/${this.props.entity.id}`
url = `rest/segments/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `/rest/segments/${this.props.list.id}`
url = `rest/segments/${this.props.list.id}`
}
try {
@ -172,7 +172,7 @@ export default class CUD extends Component {
if (submitSuccessful) {
if (stay) {
await this.getFormValuesFromURL(`/rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
await this.getFormValuesFromURL(`rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
data.rootRuleType = data.settings.rootRule.type;
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
@ -332,7 +332,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/segments/${this.props.list.id}/${this.props.entity.id}`}
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
listUrl={`/lists/${this.props.list.id}/segments`}
deletingMsg={t('Deleting segment ...')}

View file

@ -47,7 +47,7 @@ export default class List extends Component {
<Title>{t('Segment')}</Title>
<Table withHeader dataUrl={`/rest/segments-table/${this.props.list.id}`} columns={columns} />
<Table withHeader dataUrl={`rest/segments-table/${this.props.list.id}`} columns={columns} />
</div>
);
}

View file

@ -36,7 +36,7 @@ export default class CUD extends Component {
this.initForm({
serverValidation: {
url: `/rest/subscriptions-validate/${this.props.list.id}`,
url: `rest/subscriptions-validate/${this.props.list.id}`,
changed: ['email'],
extra: ['id']
},
@ -102,10 +102,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`
url = `rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `/rest/subscriptions/${this.props.list.id}`
url = `rest/subscriptions/${this.props.list.id}`
}
try {
@ -171,7 +171,7 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
actionMethod={HTTPMethod.DELETE}
actionUrl={`/rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`}
actionUrl={`rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`}
backUrl={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/edit`}
successUrl={`/lists/${this.props.list.id}/subscriptions`}
actionInProgressMsg={t('Deleting subscription ...')}

View file

@ -63,19 +63,19 @@ export default class List extends Component {
@withAsyncErrorHandler
async deleteSubscription(id) {
await axios.delete(`/rest/subscriptions/${this.props.list.id}/${id}`);
await axios.delete(getUrl(`rest/subscriptions/${this.props.list.id}/${id}`));
this.blacklistTable.refresh();
}
@withAsyncErrorHandler
async unsubscribeSubscription(id) {
await axios.post(`/rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`);
await axios.post(getUrl(`rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`));
this.blacklistTable.refresh();
}
@withAsyncErrorHandler
async blacklistSubscription(email) {
await axios.post("/rest/blacklist", { email });
await axios.post(getUrl('rest/blacklist'), { email });
this.blacklistTable.refresh();
}
@ -146,7 +146,7 @@ export default class List extends Component {
];
let dataUrl = '/rest/subscriptions-table/' + list.id;
let dataUrl = 'rest/subscriptions-table/' + list.id;
if (this.props.segmentId) {
dataUrl += '/' + this.props.segmentId;
}

View file

@ -11,6 +11,7 @@ import interoperableErrors from '../../../shared/interoperable-errors';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getGlobalNamespaceId} from "../../../shared/namespaces";
import {getUrl} from "../lib/urls";
@translate()
@withForm
@ -57,7 +58,7 @@ export default class CUD extends Component {
@withAsyncErrorHandler
async loadTreeData() {
const response = await axios.get('/rest/namespaces-tree');
const response = await axios.get(getUrl('rest/namespaces-tree'));
const data = response.data;
for (const root of data) {
root.expanded = true;
@ -112,10 +113,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/namespaces/${this.props.entity.id}`
url = `rest/namespaces/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/namespaces'
url = 'rest/namespaces'
}
try {
@ -182,7 +183,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/namespaces/${this.props.entity.id}`}
deleteUrl={`rest/namespaces/${this.props.entity.id}`}
cudUrl={`/namespaces/${this.props.entity.id}/edit`}
listUrl="/namespaces"
deletingMsg={t('Deleting namespace ...')}

View file

@ -7,6 +7,7 @@ import { TreeTable } from '../lib/tree';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import axios from '../lib/axios';
import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
@translate()
@withErrorHandling
@ -21,14 +22,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createNamespace: {
entityTypeId: 'namespace',
requiredOperations: ['createNamespace']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createNamespace
@ -72,7 +71,7 @@ export default class List extends Component {
<Title>{t('Namespaces')}</Title>
<TreeTable withHeader withDescription dataUrl="/rest/namespaces-tree" actions={actions} />
<TreeTable withHeader withDescription dataUrl="rest/namespaces-tree" actions={actions} />
</div>
);
}

View file

@ -15,7 +15,7 @@ function getMenus(t) {
':namespaceId([0-9]+)': {
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
resolve: {
namespace: params => `/rest/namespaces/${params.namespaceId}`
namespace: params => `rest/namespaces/${params.namespaceId}`
},
link: params => `/namespaces/${params.namespaceId}/edit`,
navs: {

View file

@ -14,6 +14,7 @@ import moment from 'moment';
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls";
@translate()
@withForm
@ -40,7 +41,7 @@ export default class CUD extends Component {
@withAsyncErrorHandler
async fetchUserFields(reportTemplateId) {
const result = await axios.get(`/rest/report-template-user-fields/${reportTemplateId}`);
const result = await axios.get(getUrl(`rest/report-template-user-fields/${reportTemplateId}`));
this.updateFormValue('user_fields', result.data);
}
@ -128,10 +129,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/reports/${this.props.entity.id}`
url = `rest/reports/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/reports'
url = 'rest/reports'
}
this.disableForm();
@ -191,7 +192,7 @@ export default class CUD extends Component {
if (userFieldsSpec) {
for (const spec of userFieldsSpec) {
if (spec.type === 'campaign') {
addUserFieldTableSelect(spec, '/rest/campaigns-table', 1,[
addUserFieldTableSelect(spec, 'rest/campaigns-table', 1,[
{data: 0, title: "#"},
{data: 1, title: t('Name')},
{data: 2, title: t('Description')},
@ -199,7 +200,7 @@ export default class CUD extends Component {
{data: 4, title: t('Created'), render: data => moment(data).fromNow()}
]);
} else if (spec.type === 'list') {
addUserFieldTableSelect(spec, '/rest/lists-table', 1,[
addUserFieldTableSelect(spec, 'rest/lists-table', 1,[
{data: 0, title: "#"},
{data: 1, title: t('Name')},
{data: 2, title: t('ID')},
@ -218,7 +219,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/reports/${this.props.entity.id}`}
deleteUrl={`rest/reports/${this.props.entity.id}`}
cudUrl={`/reports/${this.props.entity.id}/edit`}
listUrl="/reports"
deletingMsg={t('Deleting report ...')}
@ -231,7 +232,7 @@ export default class CUD extends Component {
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<TableSelect id="report_template" label={t('Report Template')} withHeader dropdown dataUrl="/rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
<TableSelect id="report_template" label={t('Report Template')} withHeader dropdown dataUrl="rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
<NamespaceSelect/>

View file

@ -9,6 +9,8 @@ import moment from 'moment';
import axios from '../lib/axios';
import { ReportState } from '../../../shared/reports';
import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
import {getUrl} from "../lib/urls";
@translate()
@withErrorHandling
@ -23,7 +25,7 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createReport: {
entityTypeId: 'namespace',
requiredOperations: ['createReport']
@ -39,10 +41,8 @@ export default class List extends Component {
viewReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['view']
},
};
const result = await axios.post('/rest/permissions-check', request);
}
});
this.setState({
createPermitted: result.data.createReport && result.data.executeReportTemplate,
@ -56,13 +56,13 @@ export default class List extends Component {
@withAsyncErrorHandler
async stop(table, id) {
await axios.post(`/rest/report-stop/${id}`);
await axios.post(getUrl(`rest/report-stop/${id}`));
table.refresh();
}
@withAsyncErrorHandler
async start(table, id) {
await axios.post(`/rest/report-start/${id}`);
await axios.post(getUrl(`rest/report-start/${id}`));
table.refresh();
}
@ -177,7 +177,7 @@ export default class List extends Component {
<Title>{t('Reports')}</Title>
<Table withHeader dataUrl="/rest/reports-table" columns={columns} />
<Table withHeader dataUrl="rest/reports-table" columns={columns} />
</div>
);
}

View file

@ -5,6 +5,7 @@ import { translate } from 'react-i18next';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import axios from '../lib/axios';
import {getUrl} from "../lib/urls";
@translate()
@withPageHelpers
@ -22,8 +23,8 @@ export default class Output extends Component {
@withAsyncErrorHandler
async loadOutput() {
const id = parseInt(this.props.match.params.id);
const outputRespPromise = axios.get(`/rest/report-output/${id}`);
const reportRespPromise = axios.get(`/rest/reports/${id}`);
const outputRespPromise = axios.get(getUrl(`rest/report-output/${id}`));
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
const [outputResp, reportResp] = await Promise.all([outputRespPromise, reportRespPromise]);
this.setState({

View file

@ -6,6 +6,7 @@ import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import axios from '../lib/axios';
import { ReportState } from '../../../shared/reports';
import {getUrl} from "../lib/urls";
@translate()
@withPageHelpers
@ -23,8 +24,8 @@ export default class View extends Component {
@withAsyncErrorHandler
async loadContent() {
const id = parseInt(this.props.match.params.id);
const contentRespPromise = axios.get(`/rest/report-content/${id}`);
const reportRespPromise = axios.get(`/rest/reports/${id}`);
const contentRespPromise = axios.get(getUrl(`rest/report-content/${id}`));
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
const [contentResp, reportResp] = await Promise.all([contentRespPromise, reportRespPromise]);
this.setState({

View file

@ -22,7 +22,7 @@ function getMenus(t) {
':reportId([0-9]+)': {
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
resolve: {
report: params => `/rest/reports/${params.reportId}`
report: params => `rest/reports/${params.reportId}`
},
link: params => `/reports/${params.reportId}/edit`,
navs: {
@ -69,7 +69,7 @@ function getMenus(t) {
':templateId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
resolve: {
template: params => `/rest/report-templates/${params.templateId}`
template: params => `rest/report-templates/${params.templateId}`
},
link: params => `/reports/templates/${params.templateId}/edit`,
navs: {

View file

@ -262,10 +262,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/report-templates/${this.props.entity.id}`
url = `rest/report-templates/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/report-templates'
url = 'rest/report-templates'
}
this.disableForm();
@ -275,7 +275,7 @@ export default class CUD extends Component {
if (submitSuccessful) {
if (stay) {
await this.getFormValuesFromURL(`/rest/report-templates/${this.props.entity.id}`);
await this.getFormValuesFromURL(`rest/report-templates/${this.props.entity.id}`);
this.enableForm();
this.setFormStatusMessage('success', t('Report template saved'));
} else {
@ -298,7 +298,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/reports/templates/${this.props.entity.id}`}
deleteUrl={`rest/reports/templates/${this.props.entity.id}`}
cudUrl={`/reports/templates/${this.props.entity.id}/edit`}
listUrl="/reports/templates"
deletingMsg={t('Deleting report template ...')}

View file

@ -9,6 +9,7 @@ import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import moment from 'moment';
import mailtrainConfig from 'mailtrainConfig';
import {checkPermissions} from "../../lib/permissions";
@translate()
@withPageHelpers
@ -23,14 +24,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createReportTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createReportTemplate']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess')
@ -88,7 +87,7 @@ export default class List extends Component {
<Title>{t('Report Templates')}</Title>
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} />
<Table withHeader dataUrl="rest/report-templates-table" columns={columns} />
</div>
);
}

View file

@ -1,5 +1,7 @@
'use strict';
import './lib/public-path';
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {
@ -128,7 +130,7 @@ class Root extends Component {
}
async logout() {
await axios.post('/rest/logout');
await axios.post(getUrl('rest/logout'));
window.location = getUrl();
}

View file

@ -132,10 +132,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/send-configurations/${this.props.entity.id}`
url = `rest/send-configurations/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/send-configurations'
url = 'rest/send-configurations'
}
this.disableForm();
@ -175,7 +175,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/send-configurations/${this.props.entity.id}`}
deleteUrl={`rest/send-configurations/${this.props.entity.id}`}
cudUrl={`/send-configurations/${this.props.entity.id}/edit`}
listUrl="/send-configurations"
deletingMsg={t('Deleting send configuration ...')}

View file

@ -18,6 +18,7 @@ import {Table} from '../lib/table';
import axios from '../lib/axios';
import moment from 'moment';
import {getMailerTypes} from './helpers';
import {checkPermissions} from "../lib/permissions";
@translate()
@ -35,14 +36,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createSendConfiguration: {
entityTypeId: 'namespace',
requiredOperations: ['createSendConfiguration']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createSendConfiguration
@ -96,7 +95,7 @@ export default class List extends Component {
<Title>{t('Send Configurations')}</Title>
<Table withHeader dataUrl="/rest/send-configurations-table" columns={columns} />
<Table withHeader dataUrl="rest/send-configurations-table" columns={columns} />
</div>
);
}

View file

@ -49,7 +49,6 @@ export function getMailerTypes(t) {
} else {
state.setIn([field, 'error'], null);
}
}
function getInitCommon() {

View file

@ -17,7 +17,7 @@ function getMenus(t) {
':sendConfigurationId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
resolve: {
sendConfiguration: params => `/rest/send-configurations/${params.sendConfigurationId}`
sendConfiguration: params => `rest/send-configurations/${params.sendConfigurationId}`
},
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
navs: {

View file

@ -55,10 +55,10 @@ export default class Update extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, '/rest/settings');
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, 'rest/settings');
if (submitSuccessful) {
await this.getFormValuesFromURL('/rest/settings');
await this.getFormValuesFromURL('rest/settings');
this.enableForm();
this.setFormStatusMessage('success', t('Global settings saved'));
} else {

View file

@ -9,7 +9,7 @@ function getMenus(t) {
title: t('Global Settings'),
link: '/settings',
resolve: {
configItems: params => `/rest/settings`
configItems: params => `rest/settings`
},
panelRender: props => <Update entity={props.resolved.configItems} />
}

View file

@ -11,6 +11,7 @@ import {
import { Table } from '../lib/table';
import axios from '../lib/axios';
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls";
@translate()
@withForm
@ -38,7 +39,7 @@ export default class Share extends Component {
userId
};
await axios.put('/rest/shares', data);
await axios.put(getUrl('rest/shares', data));
this.sharesTable.refresh();
this.usersTableSelect.refresh();
}
@ -78,7 +79,7 @@ export default class Share extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, '/rest/shares');
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, 'rest/shares');
if (submitSuccessful) {
this.hideFormValidation();
@ -145,8 +146,8 @@ export default class Share extends Component {
<h3 className="legend">{t('Add User')}</h3>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`/rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`/rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Share')}/>
@ -156,7 +157,7 @@ export default class Share extends Component {
<hr/>
<h3 className="legend">{t('Existing Users')}</h3>
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} />
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} />
</div>
);
}

View file

@ -9,6 +9,7 @@ import { Table } from '../lib/table';
import axios from '../lib/axios';
import mailtrainConfig from 'mailtrainConfig';
import {Icon} from "../lib/bootstrap-components";
import {getUrl} from "../lib/urls";
@translate()
@withPageHelpers
@ -33,7 +34,7 @@ export default class UserShares extends Component {
userId: this.props.user.id
};
await axios.put('/rest/shares', data);
await axios.put(getUrl('rest/shares', data));
for (const key in this.sharesTables) {
this.sharesTables[key].refresh();
}
@ -70,7 +71,7 @@ export default class UserShares extends Component {
return (
<div>
<h3>{title}</h3>
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`/rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} />
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} />
</div>
);
};

View file

@ -1,30 +1,42 @@
'use strict';
import React, { Component } from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page'
import {
withForm,
Trans,
translate
} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page'
import {
ACEEditor,
AlignedRow,
Button,
ButtonRow,
Dropdown,
Form,
FormSendMethod,
InputField,
StaticField,
TextArea,
Dropdown,
ACEEditor,
ButtonRow,
Button,
AlignedRow,
StaticField
withForm
} from '../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
import {withErrorHandling} from '../lib/error-handling';
import {
NamespaceSelect,
validateNamespace
} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import { getTemplateTypes } from './helpers';
import {getTemplateTypes} from './helpers';
import {ActionLink} from "../lib/bootstrap-components";
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls";
@translate()
@ -43,7 +55,11 @@ export default class CUD extends Component {
elementInFullscreen: false
};
this.initForm();
this.initForm({
onChangeBeforeValidation: {
type: ::this.onTypeChanged
}
});
}
static propTypes = {
@ -52,9 +68,17 @@ export default class CUD extends Component {
entity: PropTypes.object
}
onTypeChanged(mutState, key, oldType, type) {
if (type) {
this.templateTypes[type].afterTypeChange(mutState);
}
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity);
this.getFormValuesFromEntity(this.props.entity, data => {
this.templateTypes[data.type].afterLoad(data);
});
} else {
this.populateFormValues({
name: '',
@ -63,7 +87,8 @@ export default class CUD extends Component {
type: mailtrainConfig.editors[0],
text: '',
html: '',
data: {}
data: {},
...this.templateTypes[mailtrainConfig.editors[0]].initData()
});
}
}
@ -77,13 +102,18 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['type', 'value'])) {
const typeKey = state.getIn(['type', 'value']);
if (!typeKey) {
state.setIn(['type', 'error'], t('Type must be selected'));
} else {
state.setIn(['type', 'error'], null);
}
validateNamespace(t, state);
if (typeKey) {
this.templateTypes[typeKey].validate(state);
}
}
async submitHandler() {
@ -91,22 +121,23 @@ export default class CUD extends Component {
if (this.props.entity) {
const typeKey = this.getFormValue('type');
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
await this.templateTypes[typeKey].exportHTMLEditorData(this);
}
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/templates/${this.props.entity.id}`
url = `rest/templates/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/templates'
url = 'rest/templates'
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
this.templateTypes[data.type].beforeSave(data);
});
if (submitResponse) {
@ -123,7 +154,7 @@ export default class CUD extends Component {
async extractPlainText() {
const typeKey = this.getFormValue('type');
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
await this.templateTypes[typeKey].exportHTMLEditorData(this);
const html = this.getFormValue('html');
if (!html) {
@ -137,7 +168,7 @@ export default class CUD extends Component {
this.disableForm();
const response = await axios.post('/rest/html-to-text', { html });
const response = await axios.post(getUrl('rest/html-to-text', { html }));
this.updateFormValue('text', response.data.text);
@ -258,6 +289,14 @@ export default class CUD extends Component {
</div>
}
let typeForm = null;
if (typeKey) {
typeForm = <div>
{this.templateTypes[typeKey].getTypeForm(this, isEdit)}
</div>;
}
return (
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
@ -265,7 +304,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/templates/${this.props.entity.id}`}
deleteUrl={`rest/templates/${this.props.entity.id}`}
cudUrl={`/templates/${this.props.entity.id}/edit`}
listUrl="/templates"
deletingMsg={t('Deleting template ...')}
@ -287,6 +326,7 @@ export default class CUD extends Component {
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
{typeForm}
<NamespaceSelect/>

View file

@ -9,6 +9,7 @@ import { Table } from '../lib/table';
import axios from '../lib/axios';
import moment from 'moment';
import { getTemplateTypes } from './helpers';
import {checkPermissions} from "../lib/permissions";
@translate()
@withPageHelpers
@ -25,7 +26,7 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createTemplate']
@ -38,9 +39,7 @@ export default class List extends Component {
entityTypeId: 'mosaicoTemplate',
requiredOperations: ['view']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createTemplate,
@ -105,7 +104,7 @@ export default class List extends Component {
<Title>{t('Templates')}</Title>
<Table withHeader dataUrl="/rest/templates-table" columns={columns} />
<Table withHeader dataUrl="rest/templates-table" columns={columns} />
</div>
);
}

View file

@ -4,46 +4,161 @@ import React from "react";
import {
ACEEditor,
AlignedRow,
CKEditor
CKEditor,
TableSelect
} from "../lib/form";
import 'brace/mode/text';
import 'brace/mode/html'
import 'brace/mode/html';
import {MosaicoEditor, ResourceType} from "../lib/mosaico";
import {
MosaicoEditor,
ResourceType
} from "../lib/mosaico";
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
export function getTemplateTypes(t) {
const templateTypes = {};
function initFieldsIfMissing(mutState, templateType) {
const initVals = templateTypes[templateType].initData();
for (const key in initVals) {
if (!mutState.hasIn([key])) {
mutState.setIn([key, 'value'], initVals[key]);
}
}
}
function clearBeforeSave(data) {
for (const templateKey in templateTypes) {
const initVals = templateTypes[templateKey].initData();
for (const fieldKey in initVals) {
delete data[fieldKey];
}
}
}
const mosaicoTemplateTypes = getMosaicoTemplateTypes(t);
const mosaicoTemplatesColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => mosaicoTemplateTypes[data].typeName },
{ data: 5, title: t('Namespace') },
];
templateTypes.mosaico = {
typeName: t('Mosaico'),
getHTMLEditor: owner => <AlignedRow label={t('Template content (HTML)')}><MosaicoEditor ref={node => owner.editorNode = node} entity={owner.props.entity} entityTypeId={ResourceType.TEMPLATE} title={t('Mosaico Template Designer')} onFullscreenAsync={::owner.setElementInFullscreen}/></AlignedRow>,
htmlEditorBeforeSave: async owner => {
getTypeForm: (owner, isEdit) =>
<TableSelect id="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
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialModel={owner.getFormValue('mosaicoData').model}
initialMetadata={owner.getFormValue('mosaicoData').metadata}
templateId={owner.getFormValue('mosaicoTemplate')}
entityTypeId={ResourceType.TEMPLATE}
title={t('Mosaico Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}/>
</AlignedRow>,
exportHTMLEditorData: async owner => {
const {html, metadata, model} = await owner.editorNode.exportState();
owner.updateFormValue('html', html);
owner.updateFormValue('data', {metadata, model});
owner.updateFormValue('mosaicoData', {
metadata,
model
});
},
initData: () => ({
mosaicoTemplate: '',
mosaicoData: {}
}),
afterLoad: data => {
data.mosaicoTemplate = data.data.mosaicoTemplate;
data.html = data.data.html;
data.mosaicoData = {
metadata: data.data.metadata,
model: data.data.model
};
},
beforeSave: data => {
data.data = {
mosaicoTemplate: data.mosaicoTemplate,
metadata: data.mosaicoData.metadata,
model: data.mosaicoData.model
};
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, 'mosaico');
},
validate: state => {
const mosaicoTemplate = state.getIn(['mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
state.setIn(['mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
} else {
state.setIn(['mosaicoTemplate', 'error'], null);
}
}
};
templateTypes.grapejs = {
typeName: t('GrapeJS')
templateTypes.grapejs = { // TODO
typeName: t('GrapeJS'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => null,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.ckeditor = {
typeName: t('CKEditor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <CKEditor id="html" height="600px" label={t('Template content (HTML)')}/>,
htmlEditorBeforeSave: async owner => {}
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.codeeditor = {
typeName: t('Code Editor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <ACEEditor id="html" height="600px" mode="html" label={t('Template content (HTML)')}/>,
htmlEditorBeforeSave: async owner => {}
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.mjml = {
typeName: t('MJML')
templateTypes.mjml = { // TODO
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => null,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
return templateTypes;

View file

@ -124,10 +124,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/mosaico-templates/${this.props.entity.id}`
url = `rest/mosaico-templates/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/mosaico-templates'
url = 'rest/mosaico-templates'
}
this.disableForm();
@ -139,7 +139,7 @@ export default class CUD extends Component {
if (submitSuccessful) {
if (stay) {
await this.getFormValuesFromURL(`/rest/mosaico-templates/${this.props.entity.id}`, data => {
await this.getFormValuesFromURL(`rest/mosaico-templates/${this.props.entity.id}`, data => {
this.templateTypes[data.type].afterLoad(data);
});
this.enableForm();
@ -170,7 +170,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/templates/mosaico/${this.props.entity.id}`}
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
listUrl="/templates/mosaico"
deletingMsg={t('Deleting Mosaico template ...')}

View file

@ -9,6 +9,7 @@ import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import moment from 'moment';
import { getTemplateTypes } from './helpers';
import {checkPermissions} from "../../lib/permissions";
@translate()
@ -26,14 +27,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createMosaicoTemplate
@ -90,7 +89,7 @@ export default class List extends Component {
<Title>{t('Mosaico Templates')}</Title>
<Table withHeader dataUrl="/rest/mosaico-templates-table" columns={columns} />
<Table withHeader dataUrl="rest/mosaico-templates-table" columns={columns} />
</div>
);
}

View file

@ -20,7 +20,7 @@ function getMenus(t) {
':templateId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
resolve: {
template: params => `/rest/templates/${params.templateId}`
template: params => `rest/templates/${params.templateId}`
},
link: params => `/templates/${params.templateId}/edit`,
navs: {
@ -56,7 +56,7 @@ function getMenus(t) {
':mosaiceTemplateId([0-9]+)': {
title: resolved => t('Mosaico Template "{{name}}"', {name: resolved.mosaicoTemplate.name}),
resolve: {
mosaicoTemplate: params => `/rest/mosaico-templates/${params.mosaiceTemplateId}`
mosaicoTemplate: params => `rest/mosaico-templates/${params.mosaiceTemplateId}`
},
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
navs: {

View file

@ -27,7 +27,7 @@ export default class CUD extends Component {
this.initForm({
serverValidation: {
url: '/rest/users-validate',
url: 'rest/users-validate',
changed: mailtrainConfig.isAuthMethodLocal ? ['username', 'email'] : ['username'],
extra: ['id']
}
@ -133,10 +133,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/users/${this.props.entity.id}`
url = `rest/users/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/users'
url = 'rest/users'
}
try {
@ -196,7 +196,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/users/${this.props.entity.id}`}
deleteUrl={`rest/users/${this.props.entity.id}`}
cudUrl={`/users/${this.props.entity.id}/edit`}
listUrl="/users"
deletingMsg={t('Deleting user ...')}
@ -215,7 +215,7 @@ export default class CUD extends Component {
<InputField id="password2" label={t('Repeat Password')} type="password"/>
</div>
}
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={'/rest/shares-roles-table/global'} columns={rolesColumns} selectionLabelIndex={1}/>
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={'rest/shares-roles-table/global'} columns={rolesColumns} selectionLabelIndex={1}/>
<NamespaceSelect/>
<ButtonRow>

View file

@ -53,7 +53,7 @@ export default class List extends Component {
<Title>{t('Users')}</Title>
<Table withHeader dataUrl="/rest/users-table" columns={columns} />
<Table withHeader dataUrl="rest/users-table" columns={columns} />
</div>
);
}

View file

@ -20,7 +20,7 @@ function getMenus(t) {
':userId([0-9]+)': {
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
resolve: {
user: params => `/rest/users/${params.userId}`
user: params => `rest/users/${params.userId}`
},
link: params => `/users/${params.userId}/edit`,
navs: {

View file

@ -14,9 +14,9 @@ async function getAnonymousConfig(context, trusted) {
externalPasswordResetLink: config.ldap.passwordresetlink,
language: config.language || 'en',
isAuthenticated: !!context.user,
trustedUrlBase: urls.getTrustedUrl(),
trustedUrlBase: urls.getTrustedUrlBase(),
trustedUrlBaseDir: urls.getTrustedUrlBaseDir(),
sandboxUrlBase: urls.getSandboxUrl(),
sandboxUrlBase: urls.getSandboxUrlBase(),
sandboxUrlBaseDir: urls.getSandboxUrlBaseDir(),
trusted
}

View file

@ -10,9 +10,9 @@ const multer = require('multer')({
dest: uploadedFilesDir
});
function installUploadHandler(router, url, dontReplace = false) {
function installUploadHandler(router, url, getUrl = null, dontReplace = false) {
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, dontReplace));
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, getUrl, dontReplace));
});
}

View file

@ -116,16 +116,22 @@ module.exports.authByAccessToken = (req, res, next) => {
};
module.exports.tryAuthByRestrictedAccessToken = (req, res, next) => {
if (req.cookies.restricted_access_token) {
users.getByRestrictedAccessToken(req.cookies.restricted_access_token).then(user => {
req.user = user;
next();
}).catch(err => {
next();
});
} else {
const pathComps = req.url.split('/');
pathComps.shift();
const restrictedAccessToken = pathComps.shift();
pathComps.unshift('');
const url = pathComps.join('/');
req.url = url;
users.getByRestrictedAccessToken(restrictedAccessToken).then(user => {
req.user = user;
next();
}
}).catch(err => {
next();
});
};
module.exports.setup = app => {

View file

@ -3,6 +3,7 @@
const _ = require('./translate')._;
const util = require('util');
const isemail = require('isemail');
const path = require('path');
const bluebird = require('bluebird');
@ -46,7 +47,7 @@ async function getTemplate(template) {
source = compiled.html;
}
const renderer = hbs.handlebars.compile(compiled.html);
const renderer = hbs.handlebars.compile(source);
templates.set(key, renderer);
return renderer;

View file

@ -3,27 +3,41 @@
const config = require('config');
const urllib = require('url');
function getTrustedUrlBase() {
return urllib.resolve(config.www.trustedUrlBase, '');
}
function getSandboxUrlBase() {
return urllib.resolve(config.www.sandboxUrlBase, '');
}
function getTrustedUrl(path) {
return urllib.resolve(config.www.trustedUrlBase, path || '');
}
function getSandboxUrl(path) {
return urllib.resolve(config.www.sandboxUrlBase, path || '');
function getSandboxUrl(path, context) {
if (context && context.user && context.user.restrictedAccessToken) {
return urllib.resolve(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''));
} else {
return urllib.resolve(config.www.sandboxUrlBase, 'ANONYMOUS/' + (path || ''));
}
}
function getTrustedUrlBaseDir() {
const mailtrainUrl = urllib.parse(getTrustedUrl());
return mailtrainUrl.pathname;
const ivisUrl = urllib.parse(config.www.trustedUrlBase);
return ivisUrl.pathname;
}
function getSandboxUrlBaseDir() {
const mailtrainUrl = urllib.parse(getSandboxUrl());
return mailtrainUrl.pathname;
const ivisUrl = urllib.parse(config.www.sandboxUrlBase);
return ivisUrl.pathname;
}
module.exports = {
getTrustedUrl,
getSandboxUrl,
getTrustedUrlBase,
getSandboxUrlBase,
getTrustedUrlBaseDir,
getSandboxUrlBaseDir
};

View file

@ -21,6 +21,10 @@ function getFilePath(type, entityId, filename) {
return path.join(path.join(filesDir, type, entityId.toString()), filename);
}
function getFileUrl(context, type, entityId, filename, getUrl) {
return getUrl(`files/${type}/${entityId}/${filename}`, context)
}
function getFilesTable(type) {
return entityTypes[type].filesTable;
}
@ -46,7 +50,7 @@ async function getFileById(context, type, id) {
enforceTypePermitted(type);
const file = await knex.transaction(async tx => {
const file = await tx(getFilesTable(type)).where('id', id).first();
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'view');
return file;
});
@ -64,7 +68,7 @@ async function getFileById(context, type, id) {
async function getFileByFilename(context, type, entityId, name) {
enforceTypePermitted(type);
const file = await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
// XXX - Note that we don't check permissions here. This makes files generally public. However one has to know the generated name of the file.
const file = await tx(getFilesTable(type)).where({entity: entityId, filename: name}).first();
return file;
});
@ -80,7 +84,17 @@ async function getFileByFilename(context, type, entityId, name) {
};
}
async function createFiles(context, type, entityId, files, dontReplace = false) {
async function getFileByUrl(context, type, entityId, url, getUrl) {
const urlPrefix = getUrl(`files/${type}/${entityId}/`, context);
if (url.startsWith(urlPrefix)) {
const name = url.substring(urlPrefix.length);
return await getFileByFilename(context, type, entityId, name);
} else {
throw new interoperableErrors.NotFoundError();
}
}
async function createFiles(context, type, entityId, files, getUrl = null, dontReplace = false) {
enforceTypePermitted(type);
if (files.length == 0) {
// No files uploaded
@ -133,14 +147,19 @@ async function createFiles(context, type, entityId, files, dontReplace = false)
size: file.size
});
filesRet.push({
const filesRetEntry = {
name: file.filename,
originalName: originalName,
size: file.size,
type: file.mimetype,
url: `/files/${type}/${entityId}/${file.filename}`,
thumbnailUrl: `/files/${type}/${entityId}/${file.filename}` // TODO - use smaller thumbnails
});
};
if (getUrl) {
filesRetEntry.url = getFileUrl(context, type, entityId, file.filename, getUrl);
filesRetEntry.thumbnailUrl = getFileUrl(context, type, entityId, file.filename, getUrl); // TODO - use smaller thumbnails
}
filesRet.push(filesRetEntry);
if (existingNameMap.has(originalName)) {
removedFiles.push(existingNameMap.get(originalName));
@ -164,16 +183,16 @@ async function createFiles(context, type, entityId, files, dontReplace = false)
// The names should be unique, so overwrite is disabled
// The directory is created if it does not exist
// Empty options argument is passed, otherwise fails
await fs.move(file.path, filePath, {});
await fs.moveAsync(file.path, filePath, {});
}
// Remove replaced files from files directory
for (const file of removedFiles) {
const filePath = getFilePath(type, entityId, file.filename);
await fs.remove(filePath);
await fs.removeAsync(filePath);
}
// Remove ignored files from upload directory
for (const file of ignoredFiles) {
await fs.remove(file.path);
await fs.removeAsync(file.path);
}
return {
@ -194,15 +213,18 @@ async function removeFile(context, type, id) {
});
const filePath = getFilePath(type, file.entity, file.filename);
await fs.remove(filePath);
await fs.removeAsync(filePath);
}
module.exports = {
filesDir,
listDTAjax,
list,
getFileById,
getFileByFilename,
getFileByUrl,
createFiles,
removeFile,
filesDir
getFileUrl,
getFilePath
};

View file

@ -97,7 +97,6 @@ async function remove(context, id) {
});
}
module.exports = {
hash,
getById,

View file

@ -15,13 +15,16 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
async function getById(context, id) {
async function getById(context, id, withPermissions = true) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
const entity = await tx('templates').where('id', id).first();
entity.data = JSON.parse(entity.data);
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
if (withPermissions) {
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
}
return entity;
});
}

View file

@ -385,6 +385,16 @@ async function getRestrictedAccessToken(context, method, params) {
return token;
}
async function refreshRestrictedAccessToken(context, token) {
const tokenEntry = restrictedAccessTokens.get(token);
if (tokenEntry && tokenEntry.userId === context.user.id) {
tokenEntry.expires = Date.now() + 120 * 1000
} else {
shares.throwPermissionDenied();
}
}
async function getByRestrictedAccessToken(token) {
const now = Date.now();
for (const entry of restrictedAccessTokens.values()) {
@ -398,6 +408,7 @@ async function getByRestrictedAccessToken(token) {
if (tokenEntry) {
const user = await getById(contextHelpers.getAdminContext(), tokenEntry.userId);
user.restrictedAccessHandler = tokenEntry.handler;
user.restrictedAccessToken = tokenEntry.token;
return user;
@ -425,5 +436,6 @@ module.exports = {
resetPassword,
getByRestrictedAccessToken,
getRestrictedAccessToken,
refreshRestrictedAccessToken,
registerRestrictedAccessTokenMethod
};

View file

@ -3,6 +3,7 @@
const passport = require('../lib/passport');
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const { getTrustedUrl } = require('../lib/urls');
const routerFactory = require('../lib/router-async');
@ -18,7 +19,11 @@ function getRouter(trusted) {
res.render('root', {
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig)
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getTrustedUrl('mailtrain/common.js'),
getTrustedUrl('mailtrain/root.js')
]
});
});
}

View file

@ -1,6 +1,8 @@
'use strict';
const config = require('config');
const express = require('express');
const path = require('path');
const routerFactory = require('../lib/router-async');
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
@ -12,28 +14,38 @@ const users = require('../models/users');
const bluebird = require('bluebird');
const fsReadFile = bluebird.promisify(require('fs').readFile);
const path = require('path');
const files = require('../models/files');
const fileHelpers = require('../lib/file-helpers');
const templates = require('../models/templates');
const mosaicoTemplates = require('../models/mosaico-templates');
const contextHelpers = require('../lib/context-helpers');
const { getTrustedUrl, getSandboxUrl } = require('../lib/urls');
const { base } = require('../shared/templates');
users.registerRestrictedAccessTokenMethod('mosaico', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template' || entityTypeId === 'campaign') {
return {
permissions: {
[entityTypeId]: {
[entityId]: new Set(['manageFiles', 'view'])
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'mosaico') {
return {
permissions: {
'template': {
[entityId]: new Set(['manageFiles', 'view'])
},
'mosaicoTemplate': {
[tmpl.data.mosaicoTemplate]: new Set(['view'])
}
}
}
};
};
}
}
});
// FIXME - add authentication by sandboxToken
async function placeholderImage(width, height) {
const magick = gm(width, height, '#707070');
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
@ -69,9 +81,7 @@ async function placeholderImage(width, height) {
};
}
async function resizedImage(src, method, width, height) {
const filePath = path.join(__dirname, '..', src);
async function resizedImage(filePath, method, width, height) {
const magick = gm(filePath);
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
const formatAsync = bluebird.promisify(magick.format.bind(magick));
@ -114,42 +124,55 @@ function sanitizeSize(val, min, max, defaultVal, allowNull) {
function getRouter(trusted) {
const router = routerFactory.create();
const getUrl = trusted ? getTrustedUrl : getSandboxUrl;
router.getAsync('/img/:type/:entityId', passport.loggedIn, async (req, res) => {
const method = req.query.method;
const params = req.query.params;
let [width, height] = params.split(',');
let image;
if (method === 'placeholder') {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, false);
image = await placeholderImage(width, height);
} else {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, true);
const file = await files.getFileByUrl(req.context, req.params.type, req.params.entityId, req.query.src, getUrl);
image = await resizedImage(file.path, method, width, height);
}
res.set('Content-Type', 'image/' + image.format);
image.stream.pipe(res);
});
router.getAsync('/templates/:mosaicoTemplateId/index.html', passport.loggedIn, async (req, res) => {
const tmpl = await mosaicoTemplates.getById(req.context, req.params.mosaicoTemplateId);
res.set('Content-Type', 'text/html');
res.send(base(tmpl.data.html, getUrl('', req.context)));
});
router.use('/templates/:mosaicoTemplateId', express.static(path.join(__dirname, '..', 'client', 'public', 'mosaico', 'templates', 'versafix-1')));
if (!trusted) {
router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
const method = req.query.method;
const params = req.query.params;
let [width, height] = params.split(',');
let image;
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', getUrl, true);
if (method === 'placeholder') {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, false);
image = await placeholderImage(width, height);
} else {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, true);
// TODO - validate that one has the rights to read this ???
image = await resizedImage(req.query.src, method, width, height);
}
res.set('Content-Type', 'image/' + image.format);
image.stream.pipe(res);
});
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', true);
router.getAsync('/upload/:type/:fileId', passport.loggedIn, async (req, res) => {
const entries = await files.list(req.context, req.params.type, req.params.fileId);
router.getAsync('/upload/:type/:entityId', passport.loggedIn, async (req, res) => {
const entries = await files.list(req.context, req.params.type, req.params.entityId);
const filesOut = [];
for (const entry of entries) {
filesOut.push({
name: entry.originalname,
url: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}`,
url: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename, getUrl),
size: entry.size,
thumbnailUrl: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}` // TODO - use smaller thumbnails
thumbnailUrl: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename, getUrl) // TODO - use smaller thumbnails
})
}
@ -158,7 +181,6 @@ function getRouter(trusted) {
});
});
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const resourceType = req.query.type;
const resourceId = req.query.id;
@ -180,7 +202,12 @@ function getRouter(trusted) {
editorConfig: config.mosaico,
languageStrings: languageStrings,
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig)
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getUrl('mailtrain/common.js'),
getUrl('mailtrain/mosaico.js')
],
mosaicoPublicPath: getUrl('public/mosaico')
});
});
}

View file

@ -65,4 +65,9 @@ router.postAsync('/restricted-access-token', passport.loggedIn, async (req, res)
});
router.putAsync('/restricted-access-token', passport.loggedIn, async (req, res) => {
await users.refreshRestrictedAccessToken(req.context, req.body.token);
return res.json();
});
module.exports = router;

View file

@ -13,6 +13,7 @@ const _ = require('../lib/translate')._;
const contextHelpers = require('../lib/context-helpers');
const forms = require('../models/forms');
const {getTrustedUrl} = require('../lib/urls');
const bluebird = require('bluebird');
const { SubscriptionStatus } = require('../shared/lists');

View file

@ -1397,78 +1397,78 @@ const versafix = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
' <td align="right" valign="middle" class="links-color socialLinks mobile-textcenter" data-ko-display="socialIconType eq \'colors\'">\n' +
' <span data-ko-display="fbVisible" data-ko-wrap="false">&nbsp;</span>\n' +
' <a data-ko-display="fbVisible" href="" style="-ko-attr-href: @fbUrl">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/facebook_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/facebook_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="twVisible" data-ko-wrap="false">&nbsp;</span>\n' +
' <a data-ko-display="twVisible" href="" style="-ko-attr-href: @twUrl">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/twitter_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/twitter_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="ggVisible" data-ko-wrap="false">&nbsp;</span>\n' +
' <a data-ko-display="ggVisible" href="" style="-ko-attr-href: @ggUrl">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/google+_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/google+_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="webVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="webVisible" href="" style="-ko-attr-href: @webUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/web_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/web_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="inVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="inVisible" href="" style="-ko-attr-href: @inUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/linkedin_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/linkedin_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="flVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="flVisible" href="" style="-ko-attr-href: @flUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/flickr_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/flickr_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="viVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="viVisible" href="" style="-ko-attr-href: @viUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/vimeo_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/vimeo_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="instVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="instVisible" href="" style="-ko-attr-href: @instUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/instagram_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/instagram_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="youVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="youVisible" href="" style="-ko-attr-href: @youUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/youtube_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/youtube_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
' </a>\n' +
' </td>\n' +
' <td align="right" valign="middle" class="links-color socialLinks mobile-textcenter" data-ko-display="socialIconType eq \'bw\'"\n' +
' style="display: none">\n' +
' <span data-ko-display="fbVisible" data-ko-wrap="false">&nbsp;</span>\n' +
' <a data-ko-display="fbVisible" href="" style="-ko-attr-href: @fbUrl">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/facebook_bw_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/facebook_bw_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="twVisible" data-ko-wrap="false">&nbsp;</span>\n' +
' <a data-ko-display="twVisible" href="" style="-ko-attr-href: @twUrl">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/twitter_bw_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/twitter_bw_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="ggVisible" data-ko-wrap="false">&nbsp;</span>\n' +
' <a data-ko-display="ggVisible" href="" style="-ko-attr-href: @ggUrl">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/google+_bw_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/google+_bw_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="webVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="webVisible" href="" style="-ko-attr-href: @webUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/web_bw_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/web_bw_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="inVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="inVisible" href="" style="-ko-attr-href: @inUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/linkedin_bw_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/linkedin_bw_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="flVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="flVisible" href="" style="-ko-attr-href: @flUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/flickr_bw_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/flickr_bw_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="viVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="viVisible" href="" style="-ko-attr-href: @viUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/vimeo_bw_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/vimeo_bw_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="instVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="instVisible" href="" style="-ko-attr-href: @instUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/instagram_bw_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/instagram_bw_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
' </a>\n' +
' <span data-ko-display="youVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>\n' +
' <a data-ko-display="youVisible" href="" style="-ko-attr-href: @youUrl; display: none">\n' +
' <img src="[URL_BASE]/public/mosaico/img/social_def/youtube_bw_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
' <img src="img/social_def/youtube_bw_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
' </a>\n' +
' </td>\n' +
' </tr>\n' +
@ -1516,7 +1516,7 @@ const versafix = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
'\n' +
' <tr data-ko-display="_root_.sponsor.visible" style="display: none;text-align:center">\n' +
' <td align="center">\n' +
' <a href="http://www.void.it" target="_blank" rel="noreferrer"><img border="0" hspace="0" vspace="0" src="[URL_BASE]/public/mosaico/img/sponsor.gif" alt="sponsor"\n' +
' <a href="http://www.void.it" target="_blank" rel="noreferrer"><img border="0" hspace="0" vspace="0" src="img/sponsor.gif" alt="sponsor"\n' +
' style="Margin:auto;display:inline !important;" /></a>\n' +
' </td>\n' +
' </tr>\n' +

22
shared/templates.js Normal file
View file

@ -0,0 +1,22 @@
'use strict';
function base(text, baseUrl) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
}
return text.split('[URL_BASE]').join(baseUrl);
}
function unbase(text, baseUrl) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
}
return text.split(baseUrl).join('[URL_BASE]');
}
module.exports = {
base,
unbase
};

View file

@ -25,8 +25,9 @@
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>
<script src="/mailtrain/common.js"></script>
<script src="/mailtrain/root.js"></script>
{{#each scriptFiles}}
<script src="{{this}}"></script>
{{/each}}
{{/if}}
</head>

View file

@ -9,22 +9,22 @@
<title>Mailtrain</title>
<script src="/public/mosaico/vendor/jquery.min.js"></script>
<script src="/public/mosaico/vendor/jquery-migrate.min.js"></script>
<script src="/public/mosaico/vendor/knockout.js"></script>
<script src="/public/mosaico/vendor/jquery-ui.min.js"></script>
<script src="/public/mosaico/vendor/jquery.ui.touch-punch.min.js"></script>
<script src="/public/mosaico/vendor/load-image.all.min.js"></script>
<script src="/public/mosaico/vendor/canvas-to-blob.min.js"></script>
<script src="/public/mosaico/vendor/jquery.iframe-transport.js"></script>
<script src="/public/mosaico/vendor/jquery.fileupload.js"></script>
<script src="/public/mosaico/vendor/jquery.fileupload-process.js"></script>
<script src="/public/mosaico/vendor/jquery.fileupload-image.js"></script>
<script src="/public/mosaico/vendor/jquery.fileupload-validate.js"></script>
<script src="/public/mosaico/vendor/knockout-jqueryui.min.js"></script>
<script src="/public/mosaico/vendor/tinymce.min.js"></script>
<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="/public/mosaico/mosaico.min.js?v=0.16"></script>
<script src="{{mosaicoPublicPath}}/mosaico.min.js?v=0.16"></script>
{{#if languageStrings}}<script> window.mosaicoLanguageStrings = {{{languageStrings}}}; </script>{{/if}}
<script> window.mosaicoPlugins = []; </script>
@ -41,11 +41,12 @@
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>
<script src="/mailtrain/common.js"></script>
<script src="/mailtrain/mosaico.js"></script>
{{#each scriptFiles}}
<script src="{{this}}"></script>
{{/each}}
<link rel="stylesheet" href="/public/mosaico/mosaico-material.min.css?v=0.10" />
<link rel="stylesheet" href="/public/mosaico/vendor/notoregular/stylesheet.css" />
<link rel="stylesheet" href="{{mosaicoPublicPath}}/mosaico-material.min.css?v=0.10" />
<link rel="stylesheet" href="{{mosaicoPublicPath}}/vendor/notoregular/stylesheet.css" />
</head>
<body class="mo-standalone">
{{{body}}}