diff --git a/CHANGELOG.md b/CHANGELOG.md index deccc8e63a55..b82b13894e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shortcut to change color of an activated shape in new UI (Enter) (<https://github.com/opencv/cvat/pull/1683>) - Shortcut to switch split mode (<https://github.com/opencv/cvat/pull/1683>) - Built-in search for labels when create an object or change a label (<https://github.com/opencv/cvat/pull/1683>) +- Better validation of labels and attributes in raw viewer (<https://github.com/opencv/cvat/pull/1727>) ### Changed - Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>) @@ -32,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Wrong rexex for account name validation (<https://github.com/opencv/cvat/pull/1667>) - Wrong description on register view for the username field (<https://github.com/opencv/cvat/pull/1667>) - Wrong resolution for resizing a shape (<https://github.com/opencv/cvat/pull/1667>) +- React warning because of not unique keys in labels viewer (<https://github.com/opencv/cvat/pull/1727>) + ### Security - SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>) diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index d429a17f89d9..d7d6dc4ddb92 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 2a9f9705c084..e15f89230fee 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.3.0", + "version": "1.3.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/labels-editor/common.ts b/cvat-ui/src/components/labels-editor/common.ts index 5ea21f53c5e1..9627be41e3cb 100644 --- a/cvat-ui/src/components/labels-editor/common.ts +++ b/cvat-ui/src/components/labels-editor/common.ts @@ -18,6 +18,59 @@ export interface Label { let id = 0; +function validateParsedAttribute(attr: Attribute): void { + if (typeof (attr.name) !== 'string') { + throw new Error(`Type of attribute name must be a string. Got value ${attr.name}`); + } + + if (!['number', 'undefined'].includes(typeof (attr.id))) { + throw new Error(`Attribute: "${attr.name}". ` + + `Type of attribute id must be a number or undefined. Got value ${attr.id}`); + } + + if (!['checkbox', 'number', 'text', 'radio', 'select'].includes((attr.input_type || '').toLowerCase())) { + throw new Error(`Attribute: "${attr.name}". ` + + `Unknown input type: ${attr.input_type}`); + } + + if (typeof (attr.mutable) !== 'boolean') { + throw new Error(`Attribute: "${attr.name}". ` + + `Mutable flag must be a boolean value. Got value ${attr.mutable}`); + } + + if (!Array.isArray(attr.values)) { + throw new Error(`Attribute: "${attr.name}". ` + + `Attribute values must be an array. Got type ${typeof (attr.values)}`); + } + + for (const value of attr.values) { + if (typeof (value) !== 'string') { + throw new Error(`Attribute: "${attr.name}". ` + + `Each value must be a string. Got value ${value}`); + } + } +} + +export function validateParsedLabel(label: Label): void { + if (typeof (label.name) !== 'string') { + throw new Error(`Type of label name must be a string. Got value ${label.name}`); + } + + if (!['number', 'undefined'].includes(typeof (label.id))) { + throw new Error(`Label "${label.name}". ` + + `Type of label id must be only a number or undefined. Got value ${label.id}`); + } + + if (!Array.isArray(label.attributes)) { + throw new Error(`Label "${label.name}". ` + + `attributes must be an array. Got type ${typeof (label.attributes)}`); + } + + for (const attr of label.attributes) { + validateParsedAttribute(attr); + } +} + export function idGenerator(): number { return --id; } diff --git a/cvat-ui/src/components/labels-editor/raw-viewer.tsx b/cvat-ui/src/components/labels-editor/raw-viewer.tsx index a19fef874796..532046f42bbf 100644 --- a/cvat-ui/src/components/labels-editor/raw-viewer.tsx +++ b/cvat-ui/src/components/labels-editor/raw-viewer.tsx @@ -12,6 +12,8 @@ import Form, { FormComponentProps } from 'antd/lib/form/Form'; import { Label, Attribute, + validateParsedLabel, + idGenerator, } from './common'; type Props = FormComponentProps & { @@ -22,7 +24,18 @@ type Props = FormComponentProps & { class RawViewer extends React.PureComponent<Props> { private validateLabels = (_: any, value: string, callback: any): void => { try { - JSON.parse(value); + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) { + callback('Field is expected to be a JSON array'); + } + + for (const label of parsed) { + try { + validateParsedLabel(label); + } catch (error) { + callback(error.toString()); + } + } } catch (error) { callback(error.toString()); } @@ -39,7 +52,14 @@ class RawViewer extends React.PureComponent<Props> { e.preventDefault(); form.validateFields((error, values): void => { if (!error) { - onSubmit(JSON.parse(values.labels)); + const parsed = JSON.parse(values.labels); + for (const label of parsed) { + label.id = label.id || idGenerator(); + for (const attr of label.attributes) { + attr.id = attr.id || idGenerator(); + } + } + onSubmit(parsed); } }); };