Merge pull request #12551 from appsmithorg/feature/expose-post-message

feat: Add a field for the post message api exposure
This commit is contained in:
Rimil Dey 2022-06-24 12:02:59 +05:30 committed by GitHub
commit ad601f32fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 335 additions and 3 deletions

View File

@ -430,6 +430,7 @@ export const CLEAR_INTERVAL = () => `Clear interval`;
export const GET_GEO_LOCATION = () => `Get Geolocation`;
export const WATCH_GEO_LOCATION = () => `Watch Geolocation`;
export const STOP_WATCH_GEO_LOCATION = () => `Stop watching Geolocation`;
export const POST_MESSAGE = () => `Post message to a target window`;
//js actions
export const JS_ACTION_COPY_SUCCESS = (actionName: string, pageName: string) =>

View File

@ -232,6 +232,7 @@ export const ActionType = {
getGeolocation: "appsmith.geolocation.getCurrentPosition",
watchGeolocation: "appsmith.geolocation.watchPosition",
stopWatchGeolocation: "appsmith.geolocation.clearWatch",
postMessage: "postMessageToTargetWindow",
};
type ActionType = typeof ActionType[keyof typeof ActionType];
@ -355,6 +356,8 @@ export enum FieldType {
DELAY_FIELD = "DELAY_FIELD",
ID_FIELD = "ID_FIELD",
CLEAR_INTERVAL_ID_FIELD = "CLEAR_INTERVAL_ID_FIELD",
MESSAGE_FIELD = "MESSAGE_FIELD",
TARGET_ORIGIN_FIELD = "TARGET_ORIGIN_FIELD",
}
type FieldConfig = {
@ -622,6 +625,24 @@ const fieldConfigs: FieldConfigs = {
},
view: ViewTypes.TEXT_VIEW,
},
[FieldType.MESSAGE_FIELD]: {
getter: (value: string) => {
return textGetter(value, 0);
},
setter: (value: string, currentValue: string) => {
return textSetter(value, currentValue, 0);
},
view: ViewTypes.TEXT_VIEW,
},
[FieldType.TARGET_ORIGIN_FIELD]: {
getter: (value: string) => {
return textGetter(value, 1);
},
setter: (value: string, currentValue: string) => {
return textSetter(value, currentValue, 1);
},
view: ViewTypes.TEXT_VIEW,
},
};
function renderField(props: {
@ -793,6 +814,8 @@ function renderField(props: {
case FieldType.DELAY_FIELD:
case FieldType.ID_FIELD:
case FieldType.CLEAR_INTERVAL_ID_FIELD:
case FieldType.MESSAGE_FIELD:
case FieldType.TARGET_ORIGIN_FIELD:
let fieldLabel = "";
if (fieldType === FieldType.ALERT_TEXT_FIELD) {
fieldLabel = "Message";
@ -818,6 +841,10 @@ function renderField(props: {
fieldLabel = "Id";
} else if (fieldType === FieldType.CLEAR_INTERVAL_ID_FIELD) {
fieldLabel = "Id";
} else if (fieldType === FieldType.MESSAGE_FIELD) {
fieldLabel = "Message";
} else if (fieldType === FieldType.TARGET_ORIGIN_FIELD) {
fieldLabel = "Target origin";
}
viewElement = (view as (props: TextViewProps) => JSX.Element)({
label: fieldLabel,

View File

@ -49,6 +49,7 @@ import {
NAVIGATE_TO,
NO_ACTION,
OPEN_MODAL,
POST_MESSAGE,
RESET_WIDGET,
SET_INTERVAL,
SHOW_MESSAGE,
@ -125,6 +126,10 @@ const baseOptions: { label: string; value: string }[] = [
label: createMessage(STOP_WATCH_GEO_LOCATION),
value: ActionType.stopWatchGeolocation,
},
{
label: createMessage(POST_MESSAGE),
value: ActionType.postMessage,
},
];
const getBaseOptions = (featureFlags: FeatureFlags) => {
@ -373,6 +378,18 @@ function getFieldFromValue(
field: FieldType.CALLBACK_FUNCTION_FIELD,
});
}
if (value.indexOf("postMessageToTargetWindow") !== -1) {
fields.push(
{
field: FieldType.MESSAGE_FIELD,
},
{
field: FieldType.TARGET_ORIGIN_FIELD,
},
);
}
return fields;
}

View File

@ -18,6 +18,7 @@ export enum ActionTriggerType {
WATCH_CURRENT_LOCATION = "WATCH_CURRENT_LOCATION",
STOP_WATCHING_CURRENT_LOCATION = "STOP_WATCHING_CURRENT_LOCATION",
CONFIRMATION_MODAL = "CONFIRMATION_MODAL",
POST_MESSAGE = "POST_MESSAGE",
}
export const ActionTriggerFunctionNames: Record<ActionTriggerType, string> = {
@ -37,6 +38,7 @@ export const ActionTriggerFunctionNames: Record<ActionTriggerType, string> = {
[ActionTriggerType.WATCH_CURRENT_LOCATION]: "watchLocation",
[ActionTriggerType.STOP_WATCHING_CURRENT_LOCATION]: "stopWatch",
[ActionTriggerType.CONFIRMATION_MODAL]: "ConfirmationModal",
[ActionTriggerType.POST_MESSAGE]: "postMessageToTargetWindow",
};
export type RunPluginActionDescription = {
@ -166,6 +168,14 @@ export type ConfirmationModal = {
payload?: Record<string, any>;
};
export type PostMessageDescription = {
type: ActionTriggerType.POST_MESSAGE;
payload: {
message: unknown;
targetOrigin: string;
};
};
export type ActionDescription =
| RunPluginActionDescription
| ClearPluginActionDescription
@ -182,4 +192,5 @@ export type ActionDescription =
| GetCurrentLocationDescription
| WatchCurrentLocationDescription
| StopWatchingCurrentLocationDescription
| ConfirmationModal;
| ConfirmationModal
| PostMessageDescription;

View File

@ -10,8 +10,8 @@ import {
import * as log from "loglevel";
import { all, call, put, takeEvery, takeLatest } from "redux-saga/effects";
import {
evaluateArgumentSaga,
evaluateAndExecuteDynamicTrigger,
evaluateArgumentSaga,
evaluateSnippetSaga,
setAppVersionOnWorkerSaga,
} from "sagas/EvaluationsSaga";
@ -36,12 +36,12 @@ import {
logActionExecutionError,
TriggerFailureError,
UncaughtPromiseError,
UserCancelledActionExecutionError,
} from "sagas/ActionExecution/errorUtils";
import {
clearIntervalSaga,
setIntervalSaga,
} from "sagas/ActionExecution/SetIntervalSaga";
import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils";
import {
getCurrentLocationSaga,
stopWatchCurrentLocation,
@ -49,6 +49,7 @@ import {
} from "sagas/ActionExecution/GetCurrentLocationSaga";
import { requestModalConfirmationSaga } from "sagas/UtilSagas";
import { ModalType } from "reducers/uiReducers/modalActionReducer";
import { postMessageSaga } from "./PostMessageSaga";
export type TriggerMeta = {
source?: TriggerSource;
@ -142,6 +143,9 @@ export function* executeActionTriggers(
throw new UserCancelledActionExecutionError();
}
break;
case ActionTriggerType.POST_MESSAGE:
yield call(postMessageSaga, trigger.payload, triggerMeta);
break;
default:
log.error("Trigger type unknown", trigger);
throw Error("Trigger type unknown");

View File

@ -0,0 +1,60 @@
import { postMessageSaga, executePostMessage } from "./PostMessageSaga";
import { spawn } from "redux-saga/effects";
import { runSaga } from "redux-saga";
describe("PostMessageSaga", () => {
describe("postMessageSaga function", () => {
const generator = postMessageSaga(
{
message: "hello world",
targetOrigin: "https://dev.appsmith.com",
},
{},
);
it("executes postMessageSaga with the payload and trigger meta", () => {
expect(generator.next().value).toStrictEqual(
spawn(
executePostMessage,
{
message: "hello world",
targetOrigin: "https://dev.appsmith.com",
},
{},
),
);
});
it("should be done on next iteration", () => {
expect(generator.next().done).toBeTruthy();
});
});
describe("executePostMessage function", () => {
it("calls window.parent with message and target origin", () => {
const dispatched: any[] = [];
const postMessage = jest.spyOn(window.parent, "postMessage");
runSaga(
{
dispatch: (action) => dispatched.push(action),
},
executePostMessage,
{
message: "hello world",
targetOrigin: "https://dev.appsmith.com",
},
{},
);
expect(postMessage).toHaveBeenCalledWith(
"hello world",
"https://dev.appsmith.com",
undefined,
);
expect(dispatched).toEqual([]);
});
});
});

View File

@ -0,0 +1,39 @@
import { spawn } from "redux-saga/effects";
import { PostMessageDescription } from "../../entities/DataTree/actionTriggers";
import {
logActionExecutionError,
TriggerFailureError,
} from "sagas/ActionExecution/errorUtils";
import { TriggerMeta } from "./ActionExecutionSagas";
import { isEmpty } from "lodash";
export function* postMessageSaga(
payload: PostMessageDescription["payload"],
triggerMeta: TriggerMeta,
) {
yield spawn(executePostMessage, payload, triggerMeta);
}
export function* executePostMessage(
payload: PostMessageDescription["payload"],
triggerMeta: TriggerMeta,
) {
const { message, targetOrigin } = payload;
try {
if (targetOrigin === "*") {
throw new TriggerFailureError(
"Please enter a valid url as targetOrigin. Failing to provide a specific target discloses the data you send to any interested malicious site.",
);
} else if (isEmpty(targetOrigin)) {
throw new TriggerFailureError("Please enter a target origin URL.");
} else {
window.parent.postMessage(message, targetOrigin, undefined);
}
} catch (error) {
logActionExecutionError(
(error as Error).message,
triggerMeta.source,
triggerMeta.triggerPropertyName,
);
}
}

View File

@ -670,6 +670,11 @@ export const GLOBAL_FUNCTIONS = {
"!doc": "Stop executing a setInterval with id",
"!type": "fn(id: string) -> void",
},
postMessageToTargetWindow: {
"!doc":
"Establish cross-origin communication between Window objects/page and iframes",
"!type": "fn(message: unknown, targetOrigin: string)",
},
};
export const getPropsForJSActionEntity = ({

View File

@ -433,4 +433,162 @@ describe("Add functions", () => {
]),
);
});
describe("Post message to target window works", () => {
const targetOrigin = "https://dev.appsmith.com/";
it("Post message with first argument (message) as a string", () => {
const message = "Hello world!";
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: "Hello world!",
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
it("Post message with first argument (message) as undefined", () => {
const message = undefined;
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: undefined,
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
it("Post message with first argument (message) as null", () => {
const message = null;
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: null,
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
it("Post message with first argument (message) as a number", () => {
const message = 1826;
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: 1826,
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
it("Post message with first argument (message) as a boolean", () => {
const message = true;
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: true,
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
it("Post message with first argument (message) as an array", () => {
const message = [1, 2, 3, [1, 2, 3, [1, 2, 3]]];
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: [1, 2, 3, [1, 2, 3, [1, 2, 3]]],
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
it("Post message with first argument (message) as an object", () => {
const message = {
key: 1,
status: "active",
person: {
name: "timothee chalamet",
},
randomArr: [1, 2, 3],
};
expect(
dataTreeWithFunctions.postMessageToTargetWindow(message, targetOrigin),
).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: {
message: {
key: 1,
status: "active",
person: {
name: "timothee chalamet",
},
randomArr: [1, 2, 3],
},
targetOrigin: "https://dev.appsmith.com/",
},
type: "POST_MESSAGE",
}),
]),
);
});
});
});

View File

@ -261,6 +261,16 @@ const DATA_TREE_FUNCTIONS: Record<
};
},
},
postMessageToTargetWindow: function(message: unknown, targetOrigin: string) {
return {
type: ActionTriggerType.POST_MESSAGE,
payload: {
message,
targetOrigin,
},
executionType: ExecutionType.TRIGGER,
};
},
};
export const enhanceDataTreeWithFunctions = (