Some small updates coming from IVIS

This commit is contained in:
Tomas Bures 2018-07-18 18:41:18 +01:00
parent 4943b22a51
commit e85c707973
14 changed files with 4319 additions and 2975 deletions

View file

@ -174,7 +174,7 @@ function createApp(trusted) {
})); }));
if (trusted) { if (trusted) {
passport.setup(app); passport.setupRegularAuth(app);
} else { } else {
app.use(passport.tryAuthByRestrictedAccessToken); app.use(passport.tryAuthByRestrictedAccessToken);
} }

2871
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -97,7 +97,8 @@ class Button extends Component {
class DropdownMenu extends Component { class DropdownMenu extends Component {
static propTypes = { static propTypes = {
label: PropTypes.string, label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
noCaret: PropTypes.bool,
className: PropTypes.string className: PropTypes.string
} }
@ -109,10 +110,17 @@ class DropdownMenu extends Component {
className = className + ' ' + props.className; className = className + ' ' + props.className;
} }
let label;
if (this.props.noCaret) {
label = props.label;
} else {
label = <span>{props.label}{' '}<span className="caret"></span></span>;
}
return ( return (
<div className="btn-group"> <div className="btn-group">
<button type="button" className={className} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" className={className} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{props.label}{' '}<span className="caret"></span> {label}
</button> </button>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
{props.children} {props.children}
@ -140,7 +148,15 @@ class DropdownMenuItem extends Component {
return ( return (
<li className={className}> <li className={className}>
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{props.icon && <Icon icon={props.icon}/>}{props.label}{' '}<span className="caret"></span></a> {props.icon ?
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<Icon icon={props.icon}/>{' '}{props.label}{' '}<span className="caret"></span>
</a>
:
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
{props.label}{' '}<span className="caret"></span>
</a>
}
<ul className="dropdown-menu"> <ul className="dropdown-menu">
{props.children} {props.children}
</ul> </ul>

View file

@ -27,6 +27,7 @@ import styles from "./styles.scss";
import moment from "moment"; import moment from "moment";
import {getUrl} from "./urls"; import {getUrl} from "./urls";
const FormState = { const FormState = {
Loading: 0, Loading: 0,
LoadingWithNotice: 1, LoadingWithNotice: 1,
@ -76,7 +77,7 @@ class Form extends Component {
const statusMessageText = owner.getFormStatusMessageText(); const statusMessageText = owner.getFormStatusMessageText();
const statusMessageSeverity = owner.getFormStatusMessageSeverity(); const statusMessageSeverity = owner.getFormStatusMessageSeverity();
let formClass = 'form-horizontal'; let formClass = `form-horizontal ${styles.form} `;
if (props.format === 'wide') { if (props.format === 'wide') {
formClass = ''; formClass = '';
} else if (props.format === 'inline') { } else if (props.format === 'inline') {
@ -111,6 +112,7 @@ class Fieldset extends Component {
id: PropTypes.string, id: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
flat: PropTypes.bool
} }
static contextTypes = { static contextTypes = {
@ -123,7 +125,7 @@ class Fieldset extends Component {
const id = this.props.id; const id = this.props.id;
const htmlId = 'form_' + id; const htmlId = 'form_' + id;
const className = id ? owner.addFormValidationClass('', id) : ''; const className = id ? owner.addFormValidationClass('', id) : null;
let helpBlock = null; let helpBlock = null;
if (this.props.help) { if (this.props.help) {
@ -141,7 +143,7 @@ class Fieldset extends Component {
return ( return (
<fieldset className={className}> <fieldset className={className}>
{props.label ? <legend>{props.label}</legend> : null} {props.label ? <legend>{props.label}</legend> : null}
<div className="fieldset-content"> <div className={props.flat ? 'fieldset-content fieldset-content-flat' : 'fieldset-content'}>
{props.children} {props.children}
{helpBlock} {helpBlock}
{validationBlock} {validationBlock}
@ -1081,6 +1083,49 @@ function withForm(target) {
scheduleValidateForm(this); scheduleValidateForm(this);
}; };
inst.updateForm = function(mutator) {
this.setState(previousState => {
const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {};
const formState = previousState.formState.withMutations(mutState => {
mutState.update('data', stateData => stateData.withMutations(mutStateData => {
mutator(mutStateData);
if (typeof onChangeBeforeValidationCallback === 'object') {
for (const key in onChangeBeforeValidationCallback) {
const oldValue = previousState.formState.getIn(['data', key, 'value']);
const newValue = mutStateData.getIn([key, 'value']);
onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, newValue);
}
} else {
onChangeBeforeValidationCallback(mutStateData);
}
}));
validateFormState(this, mutState);
});
let newState = {
formState
};
const onChangeCallback = this.state.formSettings.onChange || {};
if (typeof onChangeCallback === 'object') {
for (const key in onChangeCallback) {
const oldValue = previousState.formState.getIn(['data', key, 'value']);
const newValue = formState.getIn(['data', key, 'value']);
onChangeCallback[key](newState, key, oldValue, newValue);
}
} else {
onChangeCallback(newState);
}
return newState;
});
};
inst.updateFormValue = function(key, value) { inst.updateFormValue = function(key, value) {
this.setState(previousState => { this.setState(previousState => {
const oldValue = previousState.formState.getIn(['data', key, 'value']); const oldValue = previousState.formState.getIn(['data', key, 'value']);

View file

@ -7,9 +7,11 @@ import ReactDOM from 'react-dom';
import {I18nextProvider,} from 'react-i18next'; import {I18nextProvider,} from 'react-i18next';
import i18n from './i18n'; import i18n from './i18n';
import {MosaicoSandbox} from './mosaico'; import {MosaicoSandbox} from './mosaico';
import {UntrustedContentRoot} from './untrusted'; import {UntrustedContentRoot, parentRPC} from './untrusted';
export default function() { export default function() {
parentRPC.init();
ReactDOM.render( ReactDOM.render(
<I18nextProvider i18n={ i18n }> <I18nextProvider i18n={ i18n }>
<UntrustedContentRoot render={props => <MosaicoSandbox {...props} />} /> <UntrustedContentRoot render={props => <MosaicoSandbox {...props} />} />

View file

@ -5,7 +5,7 @@ import {translate} from 'react-i18next';
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import styles from "./mosaico.scss"; import styles from "./mosaico.scss";
import {UntrustedContentHost} from './untrusted'; import {UntrustedContentHost, parentRPC} from './untrusted';
import {Icon} from "./bootstrap-components"; import {Icon} from "./bootstrap-components";
import { import {
getSandboxUrl, getSandboxUrl,
@ -101,7 +101,19 @@ export class MosaicoSandbox extends Component {
initialMetadata: PropTypes.string initialMetadata: PropTypes.string
} }
async exportState(method, params) {
const sandboxUrlBase = getSandboxUrl();
const trustedUrlBase = getTrustedUrl();
return {
html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, true),
model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase),
metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase)
};
}
componentDidMount() { componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
if (!Mosaico.isCompatible()) { if (!Mosaico.isCompatible()) {
alert('Update your browser!'); alert('Update your browser!');
return; return;
@ -151,23 +163,8 @@ export class MosaicoSandbox extends Component {
Mosaico.start(config, template, metadata, model, allPlugins); Mosaico.start(config, template, metadata, model, allPlugins);
} }
async onMethodAsync(method, params) {
if (method === 'exportState') {
const sandboxUrlBase = getSandboxUrl();
const trustedUrlBase = getTrustedUrl();
return {
html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, true),
model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase),
metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase)
};
}
}
render() { render() {
return <div/>; return <div/>;
} }
} }
MosaicoSandbox.prototype.onMethodAsync = async function(method, params) {
return await this.getWrappedInstance().onMethodAsync(method, params);
};

View file

@ -1,17 +1,31 @@
:global .DayPicker { .form { // This is here to give the styles below higher priority than Bootstrap has
border-left: 1px solid lightgray; :global .DayPicker {
border-right: 1px solid lightgray; border-left: 1px solid lightgray;
border-bottom: 1px solid lightgray; border-right: 1px solid lightgray;
border-radius: 4px; border-bottom: 1px solid lightgray;
padding: 15px; border-radius: 4px;
padding: 15px;
}
:global .form-horizontal .control-label {
display: block;
}
:global .form-control[disabled] {
cursor: default;
background-color: #eeeeee;
opacity: 1;
}
:global .ace_editor {
border: 1px solid #ccc;
}
} }
.dayPickerWrapper { .dayPickerWrapper {
text-align: right; text-align: right;
} }
.buttonRow > * { .buttonRow > * {
margin-right: 15px; margin-right: 15px;
} }
@ -20,6 +34,11 @@
margin-right: 0px; margin-right: 0px;
} }
.formDisabled {
background-color: #eeeeee;
opacity: 1;
}
.formStatus { .formStatus {
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
@ -33,25 +52,6 @@
margin-right: 0px; margin-right: 0px;
} }
:global .form-horizontal .control-label {
display: block;
}
.formDisabled {
background-color: #eeeeee;
opacity: 1;
}
:global .form-control[disabled] {
cursor: default;
background-color: #eeeeee;
opacity: 1;
}
:global .ace_editor {
border: 1px solid #ccc;
}
.tableSelectDropdown { .tableSelectDropdown {
margin-bottom: 15px; margin-bottom: 15px;
} }
@ -65,6 +65,7 @@
background-color: white; background-color: white;
} }
:global h3.legend { :global h3.legend {
font-size: 21px; font-size: 21px;
margin-bottom: 20px; margin-bottom: 20px;

View file

@ -324,11 +324,12 @@ class Table extends Component {
this.fetchAndNotifySelectionData(); this.fetchAndNotifySelectionData();
} }
componentDidUpdate() { componentDidUpdate(prevProps, prevState) {
if (this.props.data) { if (this.props.data) {
this.table.clear(); this.table.clear();
this.table.rows.add(this.props.data); this.table.rows.add(this.props.data);
} else { } else {
// XXX: Changing URL changing from data to dataUrl is not implemented
this.refresh(); this.refresh();
} }

View file

@ -20,7 +20,6 @@ import {
setRestrictedAccessToken setRestrictedAccessToken
} from "./urls"; } from "./urls";
@translate(null, { withRef: true })
@withPageHelpers @withPageHelpers
@withErrorHandling @withErrorHandling
@requiresAuthenticatedUser @requiresAuthenticatedUser
@ -48,16 +47,16 @@ export class UntrustedContentHost extends Component {
tokenMethod: PropTypes.string, tokenMethod: PropTypes.string,
tokenParams: PropTypes.object, tokenParams: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
singleToken: PropTypes.bool singleToken: PropTypes.bool,
onMethodAsync: PropTypes.func
} }
isInitialized() { isInitialized() {
return !!this.accessToken && !!this.props.contentProps; return !!this.accessToken && !!this.props.contentProps;
} }
receiveMessage(evt) { async receiveMessage(evt) {
const msg = evt.data; const msg = evt.data;
console.log(msg);
if (msg.type === 'initNeeded') { if (msg.type === 'initNeeded') {
if (this.isInitialized()) { if (this.isInitialized()) {
@ -69,6 +68,11 @@ export class UntrustedContentHost extends Component {
} else if (msg.type === 'rpcResponse') { } else if (msg.type === 'rpcResponse') {
const resolve = this.rpcResolves.get(msg.data.msgId); const resolve = this.rpcResolves.get(msg.data.msgId);
resolve(msg.data.ret); resolve(msg.data.ret);
} else if (msg.type === 'rpcRequest') {
const ret = await this.props.onMethodAsync(msg.data.method, msg.data.params);
} else if (msg.type === 'clientHeight') {
const newHeight = msg.data;
this.contentNode.height = newHeight;
} }
} }
@ -157,18 +161,12 @@ export class UntrustedContentHost extends Component {
} }
render() { render() {
const t = this.props.t;
return ( return (
<iframe className={styles.untrustedContent + ' ' + this.props.className} ref={node => this.contentNode = node} src={getSandboxUrl(this.props.contentSrc)} onLoad={::this.contentNodeLoaded}> </iframe> <iframe className={styles.untrustedContent + ' ' + this.props.className} ref={node => this.contentNode = node} src={getSandboxUrl(this.props.contentSrc)} onLoad={::this.contentNodeLoaded}> </iframe>
); );
} }
} }
UntrustedContentHost.prototype.ask = async function(method, params) {
return await this.getWrappedInstance().ask(method, params);
};
@translate() @translate()
export class UntrustedContentRoot extends Component { export class UntrustedContentRoot extends Component {
@ -180,6 +178,11 @@ export class UntrustedContentRoot extends Component {
}; };
this.receiveMessageHandler = ::this.receiveMessage; this.receiveMessageHandler = ::this.receiveMessage;
this.periodicTimeoutHandler = ::this.periodicTimeoutHandler;
this.periodicTimeoutId = 0;
this.clientHeight = 0;
} }
static propTypes = { static propTypes = {
@ -187,9 +190,18 @@ export class UntrustedContentRoot extends Component {
} }
async periodicTimeoutHandler() {
const newHeight = document.body.clientHeight;
if (this.clientHeight !== newHeight) {
this.clientHeight = newHeight;
this.sendMessage('clientHeight', newHeight);
}
this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250);
}
async receiveMessage(evt) { async receiveMessage(evt) {
const msg = evt.data; const msg = evt.data;
console.log(msg);
if (msg.type === 'initAvailable' && !this.state.initialized) { if (msg.type === 'initAvailable' && !this.state.initialized) {
this.sendMessage('initNeeded'); this.sendMessage('initNeeded');
@ -203,9 +215,6 @@ export class UntrustedContentRoot extends Component {
} else if (msg.type === 'accessToken') { } else if (msg.type === 'accessToken') {
setRestrictedAccessToken(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});
} }
} }
@ -215,23 +224,20 @@ export class UntrustedContentRoot extends Component {
componentDidMount() { componentDidMount() {
window.addEventListener('message', this.receiveMessageHandler, false); window.addEventListener('message', this.receiveMessageHandler, false);
this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 0);
this.sendMessage('initNeeded'); this.sendMessage('initNeeded');
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('message', this.receiveMessageHandler, false); window.removeEventListener('message', this.receiveMessageHandler, false);
clearTimeout(this.periodicTimeoutId);
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const props = {
...this.state.contentProps,
ref: node => this.contentNode = node
};
if (this.state.initialized) { if (this.state.initialized) {
return this.props.render(props); return this.props.render(this.state.contentProps);
} else { } else {
return ( return (
<div> <div>
@ -241,3 +247,82 @@ export class UntrustedContentRoot extends Component {
} }
} }
} }
class ParentRPC {
constructor(props) {
this.receiveMessageHandler = ::this.receiveMessage;
this.rpcCounter = 0;
this.rpcResolves = new Map();
this.methodHandlers = new Map();
this.initialized = false;
}
init() {
window.addEventListener('message', this.receiveMessageHandler, false);
this.initialized = true;
}
setMethodHandler(method, handler) {
this.enforceInitialized();
this.methodHandlers.set(method, handler);
}
clearMethodHandler(method) {
this.enforceInitialized();
this.methodHandlers.delete(method);
}
async ask(method, params) {
this.enforceInitialized();
this.rpcCounter += 1;
const msgId = this.rpcCounter;
this.sendMessage('rpcRequest', {
method,
params,
msgId
});
return await (new Promise((resolve, reject) => {
this.rpcResolves.set(msgId, resolve);
}));
}
// ---------------------------------------------------------------------------
// Private methods
enforceInitialized() {
if (!this.initialized) {
throw new Error('ParentRPC not initialized');
}
}
async receiveMessage(evt) {
const msg = evt.data;
if (msg.type === 'rpcResponse') {
const resolve = this.rpcResolves.get(msg.data.msgId);
resolve(msg.data.ret);
} else if (msg.type === 'rpcRequest') {
let ret;
const method = msg.data.method;
if (this.methodHandlers.has(method)) {
const handler = this.methodHandlers.get(method);
ret = await handler(method, msg.data.params);
}
this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});
}
}
sendMessage(type, data) {
window.parent.postMessage({type, data}, getTrustedUrl());
}
}
export const parentRPC = new ParentRPC();

View file

@ -134,7 +134,8 @@ module.exports.tryAuthByRestrictedAccessToken = (req, res, next) => {
}); });
}; };
module.exports.setup = app => {
module.exports.setupRegularAuth = app => {
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
}; };

View file

@ -424,11 +424,14 @@ function checkGlobalPermission(context, requiredOperations) {
} }
if (context.user.restrictedAccessHandler) { if (context.user.restrictedAccessHandler) {
log.verbose('check global permissions with restrictedAccessHandler -- requiredOperations: ' + requiredOperations); const originalRequiredOperations = requiredOperations;
const allowedPerms = context.user.restrictedAccessHandler.globalPermissions; const allowedPerms = context.user.restrictedAccessHandler.globalPermissions;
if (allowedPerms) { if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm)); requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
} else {
requiredOperations = [];
} }
log.verbose('check global permissions with restrictedAccessHandler -- requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
} }
if (requiredOperations.length === 0) { if (requiredOperations.length === 0) {
@ -471,13 +474,28 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
} }
if (context.user.restrictedAccessHandler) { if (context.user.restrictedAccessHandler) {
log.verbose('check permissions with restrictedAccessHandler -- entityTypeId: ' + entityTypeId + ' entityId: ' + entityId + ' requiredOperations: ' + requiredOperations); const originalRequiredOperations = requiredOperations;
if (context.user.restrictedAccessHandler.permissions && context.user.restrictedAccessHandler.permissions[entityTypeId]) { if (context.user.restrictedAccessHandler.permissions) {
const allowedPerms = context.user.restrictedAccessHandler.permissions[entityTypeId][entityId]; const entityPerms = context.user.restrictedAccessHandler.permissions[entityTypeId];
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm)); if (!entityPerms) {
requiredOperations = [];
} else if (entityPerms === true) {
// no change to require operations
} else if (entityPerms instanceof Set) {
requiredOperations = requiredOperations.filter(perm => entityPerms.has(perm));
} else {
const allowedPerms = entityPerms[entityId];
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
} else {
requiredOperations = [];
}
} }
} else {
requiredOperations = [];
} }
log.verbose('check permissions with restrictedAccessHandler -- entityTypeId: ' + entityTypeId + ' entityId: ' + entityId + ' requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
} }
if (requiredOperations.length === 0) { if (requiredOperations.length === 0) {

4090
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -88,7 +88,7 @@
"mailparser": "^2.0.5", "mailparser": "^2.0.5",
"marked": "^0.3.9", "marked": "^0.3.9",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"mjml": "3.3.5", "mjml": "^4.0.5",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.18.1", "moment": "^2.18.1",
"moment-timezone": "^0.5.13", "moment-timezone": "^0.5.13",

View file

@ -14,11 +14,12 @@
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz",
"integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=", "integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=",
"requires": { "requires": {
"moment": "2.20.1" "moment": ">= 2.9.0"
} }
}, },
"owasp-password-strength-test": { "owasp-password-strength-test": {
"version": "github:bures/owasp-password-strength-test#50bfcf0035b1468b9d03a00eaf561d4fed4973eb" "version": "github:bures/owasp-password-strength-test#50bfcf0035b1468b9d03a00eaf561d4fed4973eb",
"from": "github:bures/owasp-password-strength-test"
} }
} }
} }