fix: issue with navigation when operationId contains backslash or quotes (#1513)

Co-authored-by: Andrey Lomakin <lom16133@gmail.com>
Co-authored-by: Anastasiia Derymarko <anastasiia@redocly.com>
Co-authored-by: Andriy Zaleskyy <andriy.zaleskyy@redocly.com>
This commit is contained in:
Andrey Lomakin 2022-04-15 16:53:36 +03:00 committed by GitHub
parent fd8917e5c1
commit 8f7e56c747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 19 deletions

View File

@ -93,7 +93,7 @@ paths:
parameters: parameters:
- name: Accept-Language - name: Accept-Language
in: header 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 example: en-US
required: false required: false
schema: schema:
@ -182,6 +182,16 @@ paths:
} }
requestBody: requestBody:
$ref: '#/components/requestBodies/Pet' $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}': '/pet/{petId}':
get: get:
tags: tags:
@ -259,7 +269,7 @@ paths:
required: false required: false
schema: schema:
type: string type: string
example: "Bearer <TOKEN>" example: 'Bearer <TOKEN>'
- name: petId - name: petId
in: path in: path
description: Pet id to delete description: Pet id to delete
@ -432,7 +442,7 @@ paths:
application/json: application/json:
example: example:
status: 400 status: 400
message: "Invalid Order" message: 'Invalid Order'
requestBody: requestBody:
content: content:
application/json: application/json:
@ -894,11 +904,11 @@ paths:
type: string type: string
examples: examples:
response: response:
value: <Message> OK </Message> value: <Message> OK </Message>
text/plain: text/plain:
examples: examples:
response: response:
value: OK value: OK
'400': '400':
description: Invalid username/password supplied description: Invalid username/password supplied
/user/logout: /user/logout:
@ -925,7 +935,7 @@ components:
content: content:
multipart/form-data: multipart/form-data:
schema: schema:
$ref: "#/components/schemas/Cat" $ref: '#/components/schemas/Cat'
responses: responses:
'200': '200':
description: update Cat details description: update Cat details
@ -940,7 +950,7 @@ components:
content: content:
multipart/form-data: multipart/form-data:
schema: schema:
$ref: "#/components/schemas/Cat" $ref: '#/components/schemas/Cat'
responses: responses:
'200': '200':
description: create Cat details description: create Cat details
@ -1073,8 +1083,8 @@ components:
properties: properties:
id: id:
externalDocs: externalDocs:
description: "Find more info here" description: 'Find more info here'
url: "https://example.com" url: 'https://example.com'
description: Pet ID description: Pet ID
$ref: '#/components/schemas/Id' $ref: '#/components/schemas/Id'
category: category:
@ -1251,9 +1261,9 @@ webhooks:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Pet" $ref: '#/components/schemas/Pet'
responses: responses:
"200": '200':
description: Return a 200 status to indicate that the data was received successfully description: Return a 200 status to indicate that the data was received successfully
myWebhook: myWebhook:
$ref: '#/components/pathItems/webhooks' $ref: '#/components/pathItems/webhooks'

View File

@ -76,4 +76,20 @@ describe('Menu', () => {
.then($h5 => $h5[0].firstChild!.nodeValue!.trim()) .then($h5 => $h5[0].firstChild!.nodeValue!.trim())
.should('eq', 'Response Schema:'); .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');
});
}); });

View File

@ -67,7 +67,7 @@ function navigate(history: HistoryService, event: React.MouseEvent<HTMLAnchorEle
!isModifiedEvent(event) // ignore clicks with modifier keys !isModifiedEvent(event) // ignore clicks with modifier keys
) { ) {
event.preventDefault(); event.preventDefault();
history.replace(to); history.replace(encodeURI(to));
} }
} }

View File

@ -5,7 +5,7 @@ import { SpecStore } from './models';
import { history as historyInst, HistoryService } from './HistoryService'; import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils'; import { escapeHTMLAttrChars, flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder'; import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section'; export type MenuItemGroupType = 'group' | 'tag' | 'section';
@ -47,7 +47,7 @@ export class MenuStore {
if (!id) { if (!id) {
return; return;
} }
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`); scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(id)}"]`);
} }
/** /**
@ -153,7 +153,7 @@ export class MenuStore {
item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id)); item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
this.activateAndScroll(item, false); 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 { getElementAt(idx: number): Element | null {
const item = this.flatItems[idx]; 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') { if (item && item.type === 'group') {
item = item.items[0]; 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!; this.activeItemIdx = item.absoluteIdx!;
if (updateLocation) { if (updateLocation) {
this.history.replace(item.id, rewriteHistory); this.history.replace(encodeURI(item.id), rewriteHistory);
} }
item.activate(); item.activate();

View File

@ -2330,6 +2330,20 @@ and standard method from web, mobile and desktop applications.
"openapi": "3.1.0", "openapi": "3.1.0",
"paths": Object { "paths": Object {
"/pet": 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 [ "parameters": Array [
Object { Object {
"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",

View File

@ -195,8 +195,13 @@ function parseURL(url: string) {
} }
} }
export function escapeHTMLAttrChars(str: string): string {
return str.replace(/["\\]/g, '\\$&');
}
export function unescapeHTMLChars(str: string): string { export function unescapeHTMLChars(str: string): string {
return str return str
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10))) .replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)))
.replace(/&amp;/g, '&'); .replace(/&amp;/g, '&')
.replace(/&quot;/g, '"');
} }