Add post props validation (#29017)
* Add post props validation * Fix tests and revert deletion * Fix i18n * fix tests * Add some tests * Fix tests * Address feedback * Fix lint * Fix message attachments
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
01b8359347
Коммит
2e13cbb84d
@@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ProductScope} from './products';
|
||||
import {isProductScope, type ProductScope} from './products';
|
||||
import {isArrayOf, isStringArray} from './utilities';
|
||||
|
||||
export enum Permission {
|
||||
UserJoinedChannelNotification = 'user_joined_channel_notification',
|
||||
@@ -82,6 +83,72 @@ export type AppBinding = {
|
||||
submit?: AppCall;
|
||||
};
|
||||
|
||||
export function isAppBinding(obj: unknown): obj is AppBinding {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const binding = obj as AppBinding;
|
||||
|
||||
if (typeof binding.app_id !== 'string' || typeof binding.label !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.location !== undefined && typeof binding.location !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.supported_product_ids !== undefined && !isProductScope(binding.supported_product_ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.icon !== undefined && typeof binding.icon !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.hint !== undefined && typeof binding.hint !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.description !== undefined && typeof binding.description !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.role_id !== undefined && typeof binding.role_id !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.depends_on_team !== undefined && typeof binding.depends_on_team !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.depends_on_channel !== undefined && typeof binding.depends_on_channel !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.depends_on_user !== undefined && typeof binding.depends_on_user !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.depends_on_post !== undefined && typeof binding.depends_on_post !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.bindings !== undefined && !isArrayOf(binding.bindings, isAppBinding)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.form !== undefined && !isAppForm(binding.form)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.submit !== undefined && !isAppCall(binding.submit)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AppCallValues = {
|
||||
[name: string]: any;
|
||||
};
|
||||
@@ -92,6 +159,26 @@ export type AppCall = {
|
||||
state?: any;
|
||||
};
|
||||
|
||||
function isAppCall(obj: unknown): obj is AppCall {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const call = obj as AppCall;
|
||||
|
||||
if (typeof call.path !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (call.expand !== undefined && !isAppExpand(call.expand)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Here we're assuming that 'state' can be of any type, so no type check for 'state'
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AppCallRequest = AppCall & {
|
||||
context: AppContext;
|
||||
values?: AppCallValues;
|
||||
@@ -159,6 +246,64 @@ export type AppExpand = {
|
||||
locale?: AppExpandLevel;
|
||||
};
|
||||
|
||||
function isAppExpand(v: unknown): v is AppExpand {
|
||||
if (typeof v !== 'object' || v === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expand = v as AppExpand;
|
||||
|
||||
if (expand.app !== undefined && typeof expand.app !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.acting_user !== undefined && typeof expand.acting_user !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.acting_user_access_token !== undefined && typeof expand.acting_user_access_token !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.channel !== undefined && typeof expand.channel !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.config !== undefined && typeof expand.config !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.mentioned !== undefined && typeof expand.mentioned !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.parent_post !== undefined && typeof expand.parent_post !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.post !== undefined && typeof expand.post !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.root_post !== undefined && typeof expand.root_post !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.team !== undefined && typeof expand.team !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.user !== undefined && typeof expand.user !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expand.locale !== undefined && typeof expand.locale !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AppForm = {
|
||||
title?: string;
|
||||
header?: string;
|
||||
@@ -183,7 +328,78 @@ export type AppForm = {
|
||||
depends_on?: string[];
|
||||
};
|
||||
|
||||
function isAppForm(v: unknown): v is AppForm {
|
||||
if (typeof v !== 'object' || v === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const form = v as AppForm;
|
||||
|
||||
if (form.title !== undefined && typeof form.title !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.header !== undefined && typeof form.header !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.footer !== undefined && typeof form.footer !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.icon !== undefined && typeof form.icon !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.submit_buttons !== undefined && typeof form.submit_buttons !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.cancel_button !== undefined && typeof form.cancel_button !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.submit_on_cancel !== undefined && typeof form.submit_on_cancel !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.fields !== undefined && !isArrayOf(form.fields, isAppField)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.source !== undefined && !isAppCall(form.source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.submit !== undefined && !isAppCall(form.submit)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (form.depends_on !== undefined && !isStringArray(form.depends_on)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AppFormValue = string | AppSelectOption | boolean | null;
|
||||
|
||||
function isAppFormValue(v: unknown): v is AppFormValue {
|
||||
if (typeof v === 'string') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof v === 'boolean') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (v === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isAppSelectOption(v);
|
||||
}
|
||||
|
||||
export type AppFormValues = { [name: string]: AppFormValue };
|
||||
|
||||
export type AppSelectOption = {
|
||||
@@ -192,6 +408,24 @@ export type AppSelectOption = {
|
||||
icon_data?: string;
|
||||
};
|
||||
|
||||
function isAppSelectOption(v: unknown): v is AppSelectOption {
|
||||
if (typeof v !== 'object' || v === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const option = v as AppSelectOption;
|
||||
|
||||
if (typeof option.label !== 'string' || typeof option.value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (option.icon_data !== undefined && typeof option.icon_data !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AppFieldType = string;
|
||||
|
||||
// This should go in mattermost-redux
|
||||
@@ -226,6 +460,80 @@ export type AppField = {
|
||||
max_length?: number;
|
||||
};
|
||||
|
||||
function isAppField(v: unknown): v is AppField {
|
||||
if (typeof v !== 'object' || v === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const field = v as AppField;
|
||||
|
||||
if (typeof field.name !== 'string' || typeof field.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.is_required !== undefined && typeof field.is_required !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.readonly !== undefined && typeof field.readonly !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.value !== undefined && !isAppFormValue(field.value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.description !== undefined && typeof field.description !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.label !== undefined && typeof field.label !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.hint !== undefined && typeof field.hint !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.position !== undefined && typeof field.position !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.modal_label !== undefined && typeof field.modal_label !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.refresh !== undefined && typeof field.refresh !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.options !== undefined && !isArrayOf(field.options, isAppSelectOption)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.multiselect !== undefined && typeof field.multiselect !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.lookup !== undefined && !isAppCall(field.lookup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.subtype !== undefined && typeof field.subtype !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.min_length !== undefined && typeof field.min_length !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.max_length !== undefined && typeof field.max_length !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AutocompleteSuggestion = {
|
||||
suggestion: string;
|
||||
complete?: string;
|
||||
|
||||
@@ -1,27 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isArrayOf} from './utilities';
|
||||
|
||||
export type PostAction = {
|
||||
id?: string;
|
||||
id: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
style?: string;
|
||||
data_source?: string;
|
||||
options?: PostActionOption[];
|
||||
default_option?: string;
|
||||
integration?: PostActionIntegration;
|
||||
cookie?: string;
|
||||
};
|
||||
|
||||
export function isPostAction(v: unknown): v is PostAction {
|
||||
if (typeof v !== 'object' || !v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('id' in v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.id !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('name' in v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('type' in v && typeof v.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('disabled' in v && typeof v.disabled !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('style' in v && typeof v.style !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('data_source' in v && typeof v.data_source !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('options' in v && !isArrayOf(v.options, isPostActionOption)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('default_option' in v && typeof v.default_option !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('cookie' in v && typeof v.cookie !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type PostActionOption = {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type PostActionIntegration = {
|
||||
url?: string;
|
||||
context?: Record<string, any>;
|
||||
function isPostActionOption(v: unknown): v is PostActionOption {
|
||||
if (typeof v !== 'object' || !v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('text' in v && typeof v.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('value' in v && typeof v.value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type PostActionResponse = {
|
||||
|
||||
@@ -1,30 +1,141 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {PostAction} from './integration_actions';
|
||||
import {isPostAction, type PostAction} from './integration_actions';
|
||||
import {isArrayOf} from './utilities';
|
||||
|
||||
export type MessageAttachment = {
|
||||
id: number;
|
||||
fallback: string;
|
||||
color: string;
|
||||
pretext: string;
|
||||
author_name: string;
|
||||
author_link: string;
|
||||
author_icon: string;
|
||||
title: string;
|
||||
title_link: string;
|
||||
text: string;
|
||||
fields: MessageAttachmentField[];
|
||||
image_url: string;
|
||||
thumb_url: string;
|
||||
footer: string;
|
||||
footer_icon: string;
|
||||
timestamp: number | string;
|
||||
fallback?: string;
|
||||
color?: string;
|
||||
pretext?: string;
|
||||
author_name?: string;
|
||||
author_link?: string;
|
||||
author_icon?: string;
|
||||
title?: string;
|
||||
title_link?: string;
|
||||
text?: string;
|
||||
fields?: MessageAttachmentField[] | null;
|
||||
image_url?: string;
|
||||
thumb_url?: string;
|
||||
footer?: string;
|
||||
footer_icon?: string;
|
||||
actions?: PostAction[];
|
||||
};
|
||||
|
||||
export function isMessageAttachmentArray(v: unknown): v is MessageAttachment[] {
|
||||
return isArrayOf(v, isMessageAttachment);
|
||||
}
|
||||
|
||||
function isMessageAttachment(v: unknown): v is MessageAttachment {
|
||||
if (typeof v !== 'object' || !v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('fallback' in v && typeof v.fallback !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We may consider adding more validation to what color may be
|
||||
if ('color' in v && typeof v.color !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('pretext' in v && typeof v.pretext !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('author_name' in v && typeof v.author_name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Where it is used, we are calling isUrlSafe. We could consider calling it here
|
||||
if ('author_link' in v && typeof v.author_link !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We may need more validation since this is going to be passed to an img src prop
|
||||
if ('author_icon' in v && typeof v.author_icon !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('title' in v && typeof v.title !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Where it is used, we are calling isUrlSafe. We could consider calling it here
|
||||
if ('title_link' in v && typeof v.title_link !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('text' in v && typeof v.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We may need more validation since this is going to be passed to an img src prop
|
||||
if ('image_url' in v && typeof v.image_url !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We may need more validation since this is going to be passed to an img src prop
|
||||
if ('thumb_url' in v && typeof v.thumb_url !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We are truncating if the size is more than some constant. We could check this here
|
||||
if ('footer' in v && typeof v.footer !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We may need more validation since this is going to be passed to an img src prop
|
||||
if ('footer_icon' in v && typeof v.footer_icon !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('fields' in v && v.fields !== null && !isArrayOf(v.fields, isMessageAttachmentField)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('actions' in v && !isArrayOf(v.actions, isPostAction)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type MessageAttachmentField = {
|
||||
title: string;
|
||||
value: any;
|
||||
short: boolean;
|
||||
short?: boolean;
|
||||
}
|
||||
|
||||
function isMessageAttachmentField(v: unknown) {
|
||||
if (typeof v !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('title' in v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.title !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('value' in v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.value === 'object' && v.value && 'toString' in v.value && typeof v.value.toString !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('short' in v && typeof v.short !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import type {FileInfo} from './files';
|
||||
import type {Reaction} from './reactions';
|
||||
import type {TeamType} from './teams';
|
||||
import type {UserProfile} from './users';
|
||||
import type {
|
||||
RelationOneToOne,
|
||||
RelationOneToMany,
|
||||
IDMappedObjects,
|
||||
import {
|
||||
type RelationOneToOne,
|
||||
type RelationOneToMany,
|
||||
type IDMappedObjects,
|
||||
} from './utilities';
|
||||
|
||||
export type PostType = 'system_add_remove' |
|
||||
@@ -86,7 +86,7 @@ export type Post = {
|
||||
original_id: string;
|
||||
message: string;
|
||||
type: PostType;
|
||||
props: Record<string, any>;
|
||||
props: Record<string, unknown>;
|
||||
hashtags: string;
|
||||
pending_post_id: string;
|
||||
reply_count: number;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isArrayOf} from './utilities';
|
||||
|
||||
/**
|
||||
* - `null` - explicitly Channels
|
||||
* - `string` - uuid - any other product
|
||||
@@ -9,3 +11,11 @@ export type ProductIdentifier = null | string;
|
||||
|
||||
/** @see {@link ProductIdentifier} */
|
||||
export type ProductScope = ProductIdentifier | ProductIdentifier[];
|
||||
|
||||
export function isProductScope(v: unknown): v is ProductScope {
|
||||
if (v === null || typeof v === 'string') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isArrayOf(v, (e) => e === null || typeof v === 'string');
|
||||
}
|
||||
|
||||
@@ -45,3 +45,31 @@ export type Intersection<T1, T2> =
|
||||
Omit<Omit<T1&T2, keyof(Omit<T1, keyof(T2)>)>, keyof(Omit<T2, keyof(T1)>)>;
|
||||
|
||||
export type PartialExcept<T extends Record<string, unknown>, TKeysNotPartial extends keyof T> = Partial<T> & Pick<T, TKeysNotPartial>;
|
||||
|
||||
export function isArrayOf<T>(v: unknown, check: (e: unknown) => boolean): v is T[] {
|
||||
if (!Array.isArray(v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return v.every(check);
|
||||
}
|
||||
|
||||
export function isStringArray(v: unknown): v is string[] {
|
||||
return isArrayOf(v, (e) => typeof e === 'string');
|
||||
}
|
||||
|
||||
export function isRecordOf<T>(v: unknown, check: (e: unknown) => boolean): v is Record<string, T> {
|
||||
if (typeof v !== 'object' || !v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(Object.keys(v).every((k) => typeof k === 'string'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(Object.values(v).every(check))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user