From 8f7e56c747d88be5c5eb5c4bbaee0ff69e9cb2ec Mon Sep 17 00:00:00 2001 From: Andrey Lomakin Date: Fri, 15 Apr 2022 16:53:36 +0300 Subject: [PATCH] fix: issue with navigation when operationId contains backslash or quotes (#1513) Co-authored-by: Andrey Lomakin Co-authored-by: Anastasiia Derymarko Co-authored-by: Andriy Zaleskyy --- demo/openapi-3-1.yaml | 32 ++++++++++++------- e2e/integration/menu.e2e.ts | 16 ++++++++++ src/common-elements/linkify.tsx | 2 +- src/services/MenuStore.ts | 12 +++---- .../loadAndBundleSpec.test.ts.snap | 14 ++++++++ src/utils/helpers.ts | 7 +++- 6 files changed, 64 insertions(+), 19 deletions(-) diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index ac89619d..707033cf 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -93,7 +93,7 @@ paths: parameters: - name: Accept-Language in: header - description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" + description: 'The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US' example: en-US required: false schema: @@ -182,6 +182,16 @@ paths: } requestBody: $ref: '#/components/requestBodies/Pet' + delete: + tags: + - pet + summary: OperationId with quotes + operationId: deletePetBy"Id + get: + tags: + - pet + summary: OperationId with backslash + operationId: delete\PetById '/pet/{petId}': get: tags: @@ -259,7 +269,7 @@ paths: required: false schema: type: string - example: "Bearer " + example: 'Bearer ' - name: petId in: path description: Pet id to delete @@ -432,7 +442,7 @@ paths: application/json: example: status: 400 - message: "Invalid Order" + message: 'Invalid Order' requestBody: content: application/json: @@ -894,11 +904,11 @@ paths: type: string examples: response: - value: OK + value: OK text/plain: examples: response: - value: OK + value: OK '400': description: Invalid username/password supplied /user/logout: @@ -925,7 +935,7 @@ components: content: multipart/form-data: schema: - $ref: "#/components/schemas/Cat" + $ref: '#/components/schemas/Cat' responses: '200': description: update Cat details @@ -940,7 +950,7 @@ components: content: multipart/form-data: schema: - $ref: "#/components/schemas/Cat" + $ref: '#/components/schemas/Cat' responses: '200': description: create Cat details @@ -1073,8 +1083,8 @@ components: properties: id: externalDocs: - description: "Find more info here" - url: "https://example.com" + description: 'Find more info here' + url: 'https://example.com' description: Pet ID $ref: '#/components/schemas/Id' category: @@ -1251,9 +1261,9 @@ webhooks: content: application/json: schema: - $ref: "#/components/schemas/Pet" + $ref: '#/components/schemas/Pet' responses: - "200": + '200': description: Return a 200 status to indicate that the data was received successfully myWebhook: $ref: '#/components/pathItems/webhooks' diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index d958b589..22212e16 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -76,4 +76,20 @@ describe('Menu', () => { .then($h5 => $h5[0].firstChild!.nodeValue!.trim()) .should('eq', 'Response Schema:'); }); + + it('should be able to open the operation details when the operation IDs have quotes', () => { + cy.visit('e2e/standalone-3-1.html'); + cy.get('label span[title="pet"]').click({ multiple: true, force: true }); + cy.get('li').contains('OperationId with quotes').click({ multiple: true, force: true }); + cy.get('h2').contains('OperationId with quotes').should('be.visible'); + cy.url().should('include', 'deletePetBy%22Id'); + }); + + it.only('should encode URL when the operation IDs have backslashes', () => { + cy.visit('e2e/standalone-3-1.html'); + cy.get('label span[title="pet"]').click({ multiple: true, force: true }); + cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true }); + cy.get('h2').contains('OperationId with backslash').should('be.visible'); + cy.url().should('include', 'delete%5CPetById'); + }); }); diff --git a/src/common-elements/linkify.tsx b/src/common-elements/linkify.tsx index 4ace029e..bbbf2f06 100644 --- a/src/common-elements/linkify.tsx +++ b/src/common-elements/linkify.tsx @@ -67,7 +67,7 @@ function navigate(history: HistoryService, event: React.MouseEvent SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id)); this.activateAndScroll(item, false); } - this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`); + this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(id)}"]`); } }; @@ -163,7 +163,7 @@ export class MenuStore { */ getElementAt(idx: number): Element | null { const item = this.flatItems[idx]; - return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; + return (item && querySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(item.id)}"]`)) || null; } /** @@ -175,7 +175,7 @@ export class MenuStore { if (item && item.type === 'group') { item = item.items[0]; } - return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; + return (item && querySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(item.id)}"]`)) || null; } /** @@ -224,7 +224,7 @@ export class MenuStore { this.activeItemIdx = item.absoluteIdx!; if (updateLocation) { - this.history.replace(item.id, rewriteHistory); + this.history.replace(encodeURI(item.id), rewriteHistory); } item.activate(); diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index 6cd0a034..4ae0cd33 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -2330,6 +2330,20 @@ and standard method from web, mobile and desktop applications. "openapi": "3.1.0", "paths": Object { "/pet": Object { + "delete": Object { + "operationId": "deletePetBy\\"Id", + "summary": "OperationId with quotes", + "tags": Array [ + "pet", + ], + }, + "get": Object { + "operationId": "delete\\\\PetById", + "summary": "OperationId with backslash", + "tags": Array [ + "pet", + ], + }, "parameters": Array [ Object { "description": "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US", diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 32a5956f..2b7ea8e8 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -195,8 +195,13 @@ function parseURL(url: string) { } } +export function escapeHTMLAttrChars(str: string): string { + return str.replace(/["\\]/g, '\\$&'); +} + export function unescapeHTMLChars(str: string): string { return str .replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10))) - .replace(/&/g, '&'); + .replace(/&/g, '&') + .replace(/"/g, '"'); }