* 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
Этот коммит содержится в:
Daniel Espino García
2024-11-21 14:25:02 +01:00
коммит произвёл GitHub
родитель 01b8359347
Коммит 2e13cbb84d
40 изменённых файлов: 1006 добавлений и 185 удалений

Просмотреть файл

@@ -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;
}