From f20599bb543c52fb15b433dfe4113f5b3c0bb80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Tue, 22 Jun 2021 10:42:06 +0200 Subject: [PATCH] fix(v2): less strict blog/docs uri frontmatter validation (#5032) --- .../package.json | 1 + .../src/__tests__/blogFrontMatter.test.ts | 128 ++++++++++++------ .../src/blogFrontMatter.ts | 11 +- .../package.json | 1 + .../src/__tests__/docFrontMatter.test.ts | 54 +++++++- .../src/docFrontMatter.ts | 5 +- .../src/validationSchemas.ts | 3 + 7 files changed, 151 insertions(+), 52 deletions(-) diff --git a/packages/docusaurus-plugin-content-blog/package.json b/packages/docusaurus-plugin-content-blog/package.json index c4efc12952..4ff1f4dc59 100644 --- a/packages/docusaurus-plugin-content-blog/package.json +++ b/packages/docusaurus-plugin-content-blog/package.json @@ -24,6 +24,7 @@ "@docusaurus/utils": "2.0.0-beta.1", "@docusaurus/utils-validation": "2.0.0-beta.1", "chalk": "^4.1.1", + "escape-string-regexp": "^4.0.0", "feed": "^4.2.2", "fs-extra": "^10.0.0", "globby": "^11.0.2", diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/blogFrontMatter.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/blogFrontMatter.test.ts index 93e5726737..86afc79f94 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/blogFrontMatter.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/blogFrontMatter.test.ts @@ -9,6 +9,7 @@ import { BlogPostFrontMatter, validateBlogPostFrontMatter, } from '../blogFrontMatter'; +import escapeStringRegexp from 'escape-string-regexp'; function testField(params: { fieldName: keyof BlogPostFrontMatter; @@ -41,7 +42,20 @@ function testField(params: { test('throw error for values', () => { params.invalidFrontMatters?.forEach(([frontMatter, message]) => { - expect(() => validateBlogPostFrontMatter(frontMatter)).toThrow(message); + try { + validateBlogPostFrontMatter(frontMatter); + fail( + new Error( + `Blog frontmatter is expected to be rejected, but was accepted successfully:\n ${JSON.stringify( + frontMatter, + null, + 2, + )}`, + ), + ); + } catch (e) { + expect(e.message).toMatch(new RegExp(escapeStringRegexp(message))); + } }); }); }); @@ -57,7 +71,9 @@ describe('validateBlogPostFrontMatter', () => { const frontMatter = {abc: '1'}; expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter); }); +}); +describe('validateBlogPostFrontMatter description', () => { testField({ fieldName: 'description', validFrontMatters: [ @@ -66,7 +82,9 @@ describe('validateBlogPostFrontMatter', () => { {description: 'description'}, ], }); +}); +describe('validateBlogPostFrontMatter title', () => { testField({ fieldName: 'title', validFrontMatters: [ @@ -75,25 +93,25 @@ describe('validateBlogPostFrontMatter', () => { {title: 'title'}, ], }); +}); +describe('validateBlogPostFrontMatter id', () => { testField({ fieldName: 'id', validFrontMatters: [{id: '123'}, {id: 'id'}], invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']], }); +}); +describe('validateBlogPostFrontMatter author', () => { testField({ fieldName: 'author', validFrontMatters: [{author: '123'}, {author: 'author'}], invalidFrontMatters: [[{author: ''}, 'is not allowed to be empty']], }); +}); - testField({ - fieldName: 'authorTitle', - validFrontMatters: [{authorTitle: '123'}, {authorTitle: 'authorTitle'}], - invalidFrontMatters: [[{authorTitle: ''}, 'is not allowed to be empty']], - }); - +describe('validateBlogPostFrontMatter author_title', () => { testField({ fieldName: 'author_title', validFrontMatters: [{author_title: '123'}, {author_title: 'author_title'}], @@ -101,22 +119,55 @@ describe('validateBlogPostFrontMatter', () => { }); testField({ - fieldName: 'authorURL', - validFrontMatters: [{authorURL: 'https://docusaurus.io'}], + fieldName: 'authorTitle', + validFrontMatters: [{authorTitle: '123'}, {authorTitle: 'authorTitle'}], + invalidFrontMatters: [[{authorTitle: ''}, 'is not allowed to be empty']], + }); +}); + +describe('validateBlogPostFrontMatter author_url', () => { + testField({ + fieldName: 'author_url', + validFrontMatters: [ + {author_url: 'https://docusaurus.io'}, + {author_url: '../../relative'}, + {author_url: '/absolute'}, + ], invalidFrontMatters: [ - [{authorURL: ''}, 'is not allowed to be empty'], - [{authorURL: '@site/api/author'}, 'must be a valid uri'], - [{authorURL: '../../api/author'}, 'must be a valid uri'], + [ + {author_url: ''}, + '"author_url" does not match any of the allowed types', + ], ], }); testField({ - fieldName: 'author_url', - validFrontMatters: [{author_url: 'https://docusaurus.io'}], + fieldName: 'authorURL', + validFrontMatters: [ + {authorURL: 'https://docusaurus.io'}, + {authorURL: '../../relative'}, + {authorURL: '/absolute'}, + ], + invalidFrontMatters: [ - [{author_url: ''}, 'is not allowed to be empty'], - [{author_url: '@site/api/author'}, 'must be a valid uri'], - [{author_url: '../../api/author'}, 'must be a valid uri'], + [{authorURL: ''}, '"authorURL" does not match any of the allowed types'], + ], + }); +}); + +describe('validateBlogPostFrontMatter author_image_url', () => { + testField({ + fieldName: 'author_image_url', + validFrontMatters: [ + {author_image_url: 'https://docusaurus.io/asset/image.png'}, + {author_image_url: '../../relative'}, + {author_image_url: '/absolute'}, + ], + invalidFrontMatters: [ + [ + {author_image_url: ''}, + '"author_image_url" does not match any of the allowed types', + ], ], }); @@ -124,26 +175,19 @@ describe('validateBlogPostFrontMatter', () => { fieldName: 'authorImageURL', validFrontMatters: [ {authorImageURL: 'https://docusaurus.io/asset/image.png'}, + {authorImageURL: '../../relative'}, + {authorImageURL: '/absolute'}, ], invalidFrontMatters: [ - [{authorImageURL: ''}, 'is not allowed to be empty'], - [{authorImageURL: '@site/api/asset/image.png'}, 'must be a valid uri'], - [{authorImageURL: '../../api/asset/image.png'}, 'must be a valid uri'], - ], - }); - - testField({ - fieldName: 'author_image_url', - validFrontMatters: [ - {author_image_url: 'https://docusaurus.io/asset/image.png'}, - ], - invalidFrontMatters: [ - [{author_image_url: ''}, 'is not allowed to be empty'], - [{author_image_url: '@site/api/asset/image.png'}, 'must be a valid uri'], - [{author_image_url: '../../api/asset/image.png'}, 'must be a valid uri'], + [ + {authorImageURL: ''}, + '"authorImageURL" does not match any of the allowed types', + ], ], }); +}); +describe('validateBlogPostFrontMatter slug', () => { testField({ fieldName: 'slug', validFrontMatters: [ @@ -158,10 +202,13 @@ describe('validateBlogPostFrontMatter', () => { ], invalidFrontMatters: [[{slug: ''}, 'is not allowed to be empty']], }); +}); +describe('validateBlogPostFrontMatter image', () => { testField({ fieldName: 'image', validFrontMatters: [ + {image: 'https://docusaurus.io/image.png'}, {image: 'blog/'}, {image: '/blog'}, {image: '/blog/'}, @@ -172,15 +219,12 @@ describe('validateBlogPostFrontMatter', () => { {image: '@site/api/asset/image.png'}, ], invalidFrontMatters: [ - [{image: ''}, 'is not allowed to be empty'], - [{image: 'https://docusaurus.io'}, 'must be a valid relative uri'], - [ - {image: 'https://docusaurus.io/blog/image.png'}, - 'must be a valid relative uri', - ], + [{image: ''}, '"image" does not match any of the allowed types'], ], }); +}); +describe('validateBlogPostFrontMatter tags', () => { testField({ fieldName: 'tags', validFrontMatters: [ @@ -203,7 +247,9 @@ describe('validateBlogPostFrontMatter', () => { ], ], }); +}); +describe('validateBlogPostFrontMatter keywords', () => { testField({ fieldName: 'keywords', validFrontMatters: [ @@ -218,7 +264,9 @@ describe('validateBlogPostFrontMatter', () => { [{keywords: []}, 'does not contain 1 required value(s)'], ], }); +}); +describe('validateBlogPostFrontMatter draft', () => { testField({ fieldName: 'draft', validFrontMatters: [{draft: true}, {draft: false}], @@ -231,7 +279,9 @@ describe('validateBlogPostFrontMatter', () => { [{draft: 'no'}, 'must be a boolean'], ], }); +}); +describe('validateBlogPostFrontMatter hide_table_of_contents', () => { testField({ fieldName: 'hide_table_of_contents', validFrontMatters: [ @@ -247,7 +297,9 @@ describe('validateBlogPostFrontMatter', () => { [{hide_table_of_contents: 'no'}, 'must be a boolean'], ], }); +}); +describe('validateBlogPostFrontMatter date', () => { testField({ fieldName: 'date', validFrontMatters: [ diff --git a/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts b/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts index ca52a62efd..c7524d9077 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts @@ -9,6 +9,7 @@ import { JoiFrontMatter as Joi, // Custom instance for frontmatter + URISchema, validateFrontMatter, } from '@docusaurus/utils-validation'; import {Tag} from './types'; @@ -59,19 +60,19 @@ const BlogFrontMatterSchema = Joi.object({ author: Joi.string(), author_title: Joi.string(), - author_url: Joi.string().uri(), - author_image_url: Joi.string().uri(), + author_url: URISchema, + author_image_url: URISchema, slug: Joi.string(), - image: Joi.string().uri({relativeOnly: true}), + image: URISchema, keywords: Joi.array().items(Joi.string().required()), hide_table_of_contents: Joi.boolean(), // TODO re-enable warnings later, our v1 blog posts use those older frontmatter fields - authorURL: Joi.string().uri(), + authorURL: URISchema, // .warning('deprecate.error', { alternative: '"author_url"'}), authorTitle: Joi.string(), // .warning('deprecate.error', { alternative: '"author_title"'}), - authorImageURL: Joi.string().uri(), + authorImageURL: URISchema, // .warning('deprecate.error', { alternative: '"author_image_url"'}), }) .unknown() diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index e2cc41170b..8d02a1ffe2 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -32,6 +32,7 @@ "@docusaurus/utils-validation": "2.0.0-beta.1", "chalk": "^4.1.1", "combine-promises": "^1.1.0", + "escape-string-regexp": "^4.0.0", "execa": "^5.0.0", "fs-extra": "^10.0.0", "globby": "^11.0.2", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docFrontMatter.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docFrontMatter.test.ts index 489eff0881..f8f1b067a9 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docFrontMatter.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docFrontMatter.test.ts @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import {DocFrontMatter, validateDocFrontMatter} from '../docFrontMatter'; +import {validateDocFrontMatter} from '../docFrontMatter'; +import {DocFrontMatter} from '../types'; +import escapeStringRegexp from 'escape-string-regexp'; function testField(params: { fieldName: keyof DocFrontMatter; @@ -38,7 +40,20 @@ function testField(params: { test('throw error for values', () => { params.invalidFrontMatters?.forEach(([frontMatter, message]) => { - expect(() => validateDocFrontMatter(frontMatter)).toThrow(message); + try { + validateDocFrontMatter(frontMatter); + fail( + new Error( + `Doc frontmatter is expected to be rejected, but was accepted successfully:\n ${JSON.stringify( + frontMatter, + null, + 2, + )}`, + ), + ); + } catch (e) { + expect(e.message).toMatch(new RegExp(escapeStringRegexp(message))); + } }); }); }); @@ -54,13 +69,17 @@ describe('validateDocFrontMatter', () => { const frontMatter = {abc: '1'}; expect(validateDocFrontMatter(frontMatter)).toEqual(frontMatter); }); +}); +describe('validateDocFrontMatter id', () => { testField({ fieldName: 'id', validFrontMatters: [{id: '123'}, {id: 'unique_id'}], invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']], }); +}); +describe('validateDocFrontMatter title', () => { testField({ fieldName: 'title', validFrontMatters: [ @@ -69,7 +88,9 @@ describe('validateDocFrontMatter', () => { {title: 'title'}, ], }); +}); +describe('validateDocFrontMatter hide_title', () => { testField({ fieldName: 'hide_title', validFrontMatters: [{hide_title: true}, {hide_title: false}], @@ -83,7 +104,9 @@ describe('validateDocFrontMatter', () => { [{hide_title: ''}, 'must be a boolean'], ], }); +}); +describe('validateDocFrontMatter hide_table_of_contents', () => { testField({ fieldName: 'hide_table_of_contents', validFrontMatters: [ @@ -100,7 +123,9 @@ describe('validateDocFrontMatter', () => { [{hide_table_of_contents: ''}, 'must be a boolean'], ], }); +}); +describe('validateDocFrontMatter keywords', () => { testField({ fieldName: 'keywords', validFrontMatters: [ @@ -115,18 +140,23 @@ describe('validateDocFrontMatter', () => { [{keywords: []}, 'does not contain 1 required value(s)'], ], }); +}); +describe('validateDocFrontMatter image', () => { testField({ fieldName: 'image', - validFrontMatters: [{image: 'https://docusaurus.io/blog/image.png'}], + validFrontMatters: [ + {image: 'https://docusaurus.io/blog/image.png'}, + {image: '/absolute/image.png'}, + {image: '../relative/image.png'}, + ], invalidFrontMatters: [ - [{image: ''}, 'is not allowed to be empty'], - [{image: './api/@docusaurus/plugin-debug'}, 'must be a valid uri'], - [{image: '/api/@docusaurus/plugin-debug'}, 'must be a valid uri'], - [{image: '@site/api/asset/image.png'}, 'must be a valid uri'], + [{image: ''}, 'does not match any of the allowed types'], ], }); +}); +describe('validateDocFrontMatter description', () => { testField({ fieldName: 'description', validFrontMatters: [ @@ -135,7 +165,9 @@ describe('validateDocFrontMatter', () => { {description: 'description'}, ], }); +}); +describe('validateDocFrontMatter slug', () => { testField({ fieldName: 'slug', validFrontMatters: [ @@ -150,13 +182,17 @@ describe('validateDocFrontMatter', () => { ], invalidFrontMatters: [[{slug: ''}, 'is not allowed to be empty']], }); +}); +describe('validateDocFrontMatter sidebar_label', () => { testField({ fieldName: 'sidebar_label', validFrontMatters: [{sidebar_label: 'Awesome docs'}], invalidFrontMatters: [[{sidebar_label: ''}, 'is not allowed to be empty']], }); +}); +describe('validateDocFrontMatter sidebar_position', () => { testField({ fieldName: 'sidebar_position', validFrontMatters: [ @@ -172,7 +208,9 @@ describe('validateDocFrontMatter', () => { [{sidebar_position: -1}, 'must be greater than or equal to 0'], ], }); +}); +describe('validateDocFrontMatter custom_edit_url', () => { testField({ fieldName: 'custom_edit_url', validFrontMatters: [ @@ -184,7 +222,9 @@ describe('validateDocFrontMatter', () => { {custom_edit_url: '@site/api/docs/markdown.md'}, ], }); +}); +describe('validateDocFrontMatter parse_number_prefixes', () => { testField({ fieldName: 'parse_number_prefixes', validFrontMatters: [ diff --git a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts index 40cd4e4509..2def606120 100644 --- a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts @@ -9,6 +9,7 @@ import { JoiFrontMatter as Joi, // Custom instance for frontmatter + URISchema, validateFrontMatter, } from '@docusaurus/utils-validation'; import {DocFrontMatter} from './types'; @@ -23,13 +24,13 @@ const DocFrontMatterSchema = Joi.object({ hide_title: Joi.boolean(), hide_table_of_contents: Joi.boolean(), keywords: Joi.array().items(Joi.string().required()), - image: Joi.string().uri({allowRelative: false}), + image: URISchema, description: Joi.string().allow(''), // see https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398 slug: Joi.string(), sidebar_label: Joi.string(), sidebar_position: Joi.number().min(0), pagination_label: Joi.string(), - custom_edit_url: Joi.string().uri({allowRelative: true}).allow('', null), + custom_edit_url: URISchema.allow('', null), parse_number_prefixes: Joi.boolean(), }).unknown(); diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts index efe8420da9..304a391ab1 100644 --- a/packages/docusaurus-utils-validation/src/validationSchemas.ts +++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts @@ -25,8 +25,11 @@ export const RehypePluginsSchema = MarkdownPluginsSchema; export const AdmonitionsSchema = Joi.object().default({}); +// TODO how can we make this emit a custom error message :'( +// Joi is such a pain, good luck to annoying trying to improve this export const URISchema = Joi.alternatives( Joi.string().uri({allowRelative: true}), + // This custom validation logic is required notably because Joi does not accept paths like /a/b/c ... Joi.custom((val, helpers) => { try { const url = new URL(val);