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) {
passport.setup(app);
passport.setupRegularAuth(app);
} else {
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 {
static propTypes = {
label: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
noCaret: PropTypes.bool,
className: PropTypes.string
}
@ -109,10 +110,17 @@ class DropdownMenu extends Component {
className = className + ' ' + props.className;
}
let label;
if (this.props.noCaret) {
label = props.label;
} else {
label = <span>{props.label}{' '}<span className="caret"></span></span>;
}
return (
<div className="btn-group">
<button type="button" className={className} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{props.label}{' '}<span className="caret"></span>
{label}
</button>
<ul className="dropdown-menu">
{props.children}
@ -140,7 +148,15 @@ class DropdownMenuItem extends Component {
return (
<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">
{props.children}
</ul>

View file

@ -27,6 +27,7 @@ import styles from "./styles.scss";
import moment from "moment";
import {getUrl} from "./urls";
const FormState = {
Loading: 0,
LoadingWithNotice: 1,
@ -76,7 +77,7 @@ class Form extends Component {
const statusMessageText = owner.getFormStatusMessageText();
const statusMessageSeverity = owner.getFormStatusMessageSeverity();
let formClass = 'form-horizontal';
let formClass = `form-horizontal ${styles.form} `;
if (props.format === 'wide') {
formClass = '';
} else if (props.format === 'inline') {
@ -111,6 +112,7 @@ class Fieldset extends Component {
id: PropTypes.string,
label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
flat: PropTypes.bool
}
static contextTypes = {
@ -123,7 +125,7 @@ class Fieldset extends Component {
const id = this.props.id;
const htmlId = 'form_' + id;
const className = id ? owner.addFormValidationClass('', id) : '';
const className = id ? owner.addFormValidationClass('', id) : null;
let helpBlock = null;
if (this.props.help) {
@ -141,7 +143,7 @@ class Fieldset extends Component {
return (
<fieldset className={className}>
{props.label ? <legend>{props.label}</legend> : null}
<div className="fieldset-content">
<div className={props.flat ? 'fieldset-content fieldset-content-flat' : 'fieldset-content'}>
{props.children}
{helpBlock}
{validationBlock}
@ -1081,6 +1083,49 @@ function withForm(target) {
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) {
this.setState(previousState => {
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 i18n from './i18n';
import {MosaicoSandbox} from './mosaico';
import {UntrustedContentRoot} from './untrusted';
import {UntrustedContentRoot, parentRPC} from './untrusted';
export default function() {
parentRPC.init();
ReactDOM.render(
<I18nextProvider i18n={ i18n }>
<UntrustedContentRoot render={props => <MosaicoSandbox {...props} />} />

View file

@ -5,7 +5,7 @@ import {translate} from 'react-i18next';
import PropTypes from "prop-types";
import styles from "./mosaico.scss";
import {UntrustedContentHost} from './untrusted';
import {UntrustedContentHost, parentRPC} from './untrusted';
import {Icon} from "./bootstrap-components";
import {
getSandboxUrl,
@ -101,7 +101,19 @@ export class MosaicoSandbox extends Component {
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() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
if (!Mosaico.isCompatible()) {
alert('Update your browser!');
return;
@ -151,23 +163,8 @@ export class MosaicoSandbox extends Component {
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() {
return <div/>;
}
}
MosaicoSandbox.prototype.onMethodAsync = async function(method, params) {
return await this.getWrappedInstance().onMethodAsync(method, params);
};

View file

@ -1,17 +1,31 @@
:global .DayPicker {
border-left: 1px solid lightgray;
border-right: 1px solid lightgray;
border-bottom: 1px solid lightgray;
border-radius: 4px;
padding: 15px;
.form { // This is here to give the styles below higher priority than Bootstrap has
:global .DayPicker {
border-left: 1px solid lightgray;
border-right: 1px solid lightgray;
border-bottom: 1px solid lightgray;
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 {
text-align: right;
}
.buttonRow > * {
margin-right: 15px;
}
@ -20,6 +34,11 @@
margin-right: 0px;
}
.formDisabled {
background-color: #eeeeee;
opacity: 1;
}
.formStatus {
padding-top: 5px;
padding-bottom: 5px;
@ -33,25 +52,6 @@
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 {
margin-bottom: 15px;
}
@ -65,6 +65,7 @@
background-color: white;
}
:global h3.legend {
font-size: 21px;
margin-bottom: 20px;

View file

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

View file

@ -20,7 +20,6 @@ import {
setRestrictedAccessToken
} from "./urls";
@translate(null, { withRef: true })
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -48,16 +47,16 @@ export class UntrustedContentHost extends Component {
tokenMethod: PropTypes.string,
tokenParams: PropTypes.object,
className: PropTypes.string,
singleToken: PropTypes.bool
singleToken: PropTypes.bool,
onMethodAsync: PropTypes.func
}
isInitialized() {
return !!this.accessToken && !!this.props.contentProps;
}
receiveMessage(evt) {
async receiveMessage(evt) {
const msg = evt.data;
console.log(msg);
if (msg.type === 'initNeeded') {
if (this.isInitialized()) {
@ -69,6 +68,11 @@ export class UntrustedContentHost extends Component {
} else if (msg.type === 'rpcResponse') {
const resolve = this.rpcResolves.get(msg.data.msgId);
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() {
const t = this.props.t;
return (
<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()
export class UntrustedContentRoot extends Component {
@ -180,6 +178,11 @@ export class UntrustedContentRoot extends Component {
};
this.receiveMessageHandler = ::this.receiveMessage;
this.periodicTimeoutHandler = ::this.periodicTimeoutHandler;
this.periodicTimeoutId = 0;
this.clientHeight = 0;
}
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) {
const msg = evt.data;
console.log(msg);
if (msg.type === 'initAvailable' && !this.state.initialized) {
this.sendMessage('initNeeded');
@ -203,9 +215,6 @@ export class UntrustedContentRoot extends Component {
} else if (msg.type === 'accessToken') {
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() {
window.addEventListener('message', this.receiveMessageHandler, false);
this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 0);
this.sendMessage('initNeeded');
}
componentWillUnmount() {
window.removeEventListener('message', this.receiveMessageHandler, false);
clearTimeout(this.periodicTimeoutId);
}
render() {
const t = this.props.t;
const props = {
...this.state.contentProps,
ref: node => this.contentNode = node
};
if (this.state.initialized) {
return this.props.render(props);
return this.props.render(this.state.contentProps);
} else {
return (
<div>
@ -240,4 +246,83 @@ 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.session());
};

View file

@ -424,11 +424,14 @@ function checkGlobalPermission(context, requiredOperations) {
}
if (context.user.restrictedAccessHandler) {
log.verbose('check global permissions with restrictedAccessHandler -- requiredOperations: ' + requiredOperations);
const originalRequiredOperations = requiredOperations;
const allowedPerms = context.user.restrictedAccessHandler.globalPermissions;
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
} else {
requiredOperations = [];
}
log.verbose('check global permissions with restrictedAccessHandler -- requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
}
if (requiredOperations.length === 0) {
@ -471,13 +474,28 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
}
if (context.user.restrictedAccessHandler) {
log.verbose('check permissions with restrictedAccessHandler -- entityTypeId: ' + entityTypeId + ' entityId: ' + entityId + ' requiredOperations: ' + requiredOperations);
if (context.user.restrictedAccessHandler.permissions && context.user.restrictedAccessHandler.permissions[entityTypeId]) {
const allowedPerms = context.user.restrictedAccessHandler.permissions[entityTypeId][entityId];
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
const originalRequiredOperations = requiredOperations;
if (context.user.restrictedAccessHandler.permissions) {
const entityPerms = context.user.restrictedAccessHandler.permissions[entityTypeId];
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) {

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",
"marked": "^0.3.9",
"memory-cache": "^0.2.0",
"mjml": "3.3.5",
"mjml": "^4.0.5",
"mkdirp": "^0.5.1",
"moment": "^2.18.1",
"moment-timezone": "^0.5.13",

View file

@ -14,11 +14,12 @@
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz",
"integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=",
"requires": {
"moment": "2.20.1"
"moment": ">= 2.9.0"
}
},
"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"
}
}
}