mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-14 04:46: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:
|
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'
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(/&/g, '&');
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user