mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-10 19:06:34 +03:00
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:
parent
fd8917e5c1
commit
8f7e56c747
|
@ -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 <TOKEN>"
|
||||
example: 'Bearer <TOKEN>'
|
||||
- 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: <Message> OK </Message>
|
||||
value: <Message> OK </Message>
|
||||
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'
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,7 +67,7 @@ function navigate(history: HistoryService, event: React.MouseEvent<HTMLAnchorEle
|
|||
!isModifiedEvent(event) // ignore clicks with modifier keys
|
||||
) {
|
||||
event.preventDefault();
|
||||
history.replace(to);
|
||||
history.replace(encodeURI(to));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { SpecStore } from './models';
|
|||
import { history as historyInst, HistoryService } from './HistoryService';
|
||||
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';
|
||||
|
||||
export type MenuItemGroupType = 'group' | 'tag' | 'section';
|
||||
|
@ -47,7 +47,7 @@ export class MenuStore {
|
|||
if (!id) {
|
||||
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));
|
||||
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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, '"');
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user