Use Firefox schemas for manifest validation (#1238)

Fixes #1131.
Fixes #1196.

* Add comments about FLAG_PATTERN_REWRITES
This commit is contained in:
Mark Striemer 2017-04-21 12:50:18 -05:00 коммит произвёл GitHub
Родитель ce9bc680d9
Коммит 7e59f35dfb
12 изменённых файлов: 94 добавлений и 308 удалений

Просмотреть файл

@ -27,6 +27,3 @@ env:
- secure: kxnTz51/kPI6y4vU8y222rX1B0csVWXU3pvJVzKxgBuoOtvSsebqsjPXkBT67j0anf7yg/N9qIFXf/8J2QWOAs3mYS0viONppGsiAUqla9QwpEJG89equl/760J5RIiSswsMTI/Rd1CM53AF7lUudF5+htXt9VkWvOAbJzkCTAdsqJZgNIQYDezkRxEwpmaIVZNzh/wsjW7x8as4sAeq3qZeVGRA6skEJiOIoR780EFOQqVz+BH+17NreFFM60ekkLBTzjCjkRyWCZmNl8FMz/F/E3qN/21y6ggqfBJf9qGO9GJ3xAr5FhGmyllMP4xKyQeBlUkbzJHWFnAvJFOQ+4dlVech9X3RFczQwMpnVT/m6rQyYy7xQXw+2f6iiAXf4biPsWJgEOBuJwgNIIGAWTbxvH7k5xa5iDbKFntw0TIzAopNZI6JBd5kUQHri0544q2akHRZzcpa+0ZBqOiqMMoL3pkws2L8E8VYKofL3vmP/2o298Ah0KYZWsl6Oc5Ev/vfQLn+cGROpgoeoURW601kFpJ4+uLNhQwdxavEI0KnRLK9OZKik0mHzy/LX/xp6aKl1jREc04pONzSHqQtkhSLmjauQoF0e6UXWCPdH4LW1bd1Xw0BhvyOjSiy2gOyRfj9WHunn9pb4oDsYQbswDIlew6wQR6uKoc7d7/rUDI=
- secure: h4pyX3Ak/ZpKyMWUKOcdng6Psq1FqAsTG0ErXUwxWQ16ijHZZL5jlcMnDpH9ctRjyaS8s/3dfa2dRAuPCt9cQOBXZBWNCGekr5kpVq8eaYDJ1SOzEvm8CPtNkqof46ifYN+KDFtqodg83M9Mm2ab0MZgvCP7JJd072SnafhQvXyZ4rOucocnbAp31BFtHhMLlGpI+Vr/1jl88Df1tHDyiPdVjqzNm0z9CGttFkmOpO9Uah+6hKKslDZGtFiRxnf4rpiWSYKkkWe9pa1330V37Cfmz8T4k4c8irg2Hi3ShojAmgRNuwq6Dg8skzy61R172FWpb6KhmT/usRX15FQ005gkM4NXJ3oRNORgsjiUtCKHDFlCJiFO/suqIcWxvEPvQjBJgAL+efxxku2S9wUGoLk3mw2KCHuw+Q/tj+kaqtO5XbtUH3h43M6UqC37E+ZLMnQnQx4I7YcsfCY54pwxbwgRDm/k/5Egs596fKxUJO4VBY04FdB1tqxleVG1GN3fv6qfcZaUDj3VPEQ1d5jFzHcGm5QVEbsyfh/FSdgUmMTuWtC8orqIZ8Rt774HJjzjUw9SkiBCQMk5dI5vi8vMbf3JHQVqQW+LdDW+Z5qd4ZlVYqPOK/FBspq54qpIFIA+6mmmUmzJOzpmEokNhoDq3gBGBDD+gqXJpQ6Vi5qe1/E=
- secure: R5a9OOHyki7lihwtbWuWaIBv7nwKY8lCGRmYaSiZ5rKZ5ZSkaoC55eq3JsFqW1sQTxsiri6UJKBXoxJJiY1veV5DVuER6ZRoYd3Vr55BjKMBBFSp5uTFrzX7Mtbfncuf5/tVbwoDxDgc/AwahbWvR/gV9yUDGrbMrgU1/jE0Otjw+LXEqc0+nINoMVyfYFT9Elt1zcl6iKIKS+LDO0S9B5jQpxyIudC2bh/k8uaSKYRaBG9srqMuPJKXOdcMxmtOLkjcr7pKT8+LL7Y6U9i/tpsYnXHv/hz6m084Wz/Eu634wPjEfOTExoXgH1frxGF24ZgmdSfo6xm3fjI+1FPyyB7kp+y4r/pyySjHtqWJgIIUR1no22ISA0z6vy/mtQ0IXPgqoXoF96F2U2hyqVp9Mu5IXASiJXGvafW/bq4cstwRNibC8llmT5QJwJFZLs47qwCroLnyapr8Zbx6+4vzqTX6jQDURy8b95ydewtMhmwLeTeOhX+3qWnYqkG/+thhAkk1bjnuZkFgLMx9ae7GaZJSXelxhLR3pSpr9yGQ/WGnWjeumk3bN/gM8c94VCv2WDFBqb4bOjYsHZk+gGkh4mr1MznrjHcf6K7QJf9eVOXGwq4F/AMAqNkYcx3qj02b2PRrZ1WvF4MPGOw2PHREBLUzIiJmYl/iopT76XBo+f8=
matrix:
- USE_FIREFOX_SCHEMAS=
- USE_FIREFOX_SCHEMAS=true

Просмотреть файл

@ -8,6 +8,21 @@ import request from 'request';
import tar from 'tar';
const FLAG_PATTERN_REGEX = /^\(\?[im]*\)(.*)/;
/* There are some patterns in the Firefox schemas that have case insensitive
* flags set. These are marked with (?i) at the beginning of them. The JSON
* Schema spec does not support flags so this object defines rewritten versions
* of patterns without the flags. Since these need to be managed by hand, the
* code that detects a flag in a pattern will throw if there is no rewritten
* pattern for it, preventing updates to the schemas until it is fixed. */
/* eslint-disable max-len */
export const FLAG_PATTERN_REWRITES = {
// Extension ID, UUID format.
'(?i)^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$':
'^\\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\}$',
// Extension ID, email format.
'(?i)^[a-z0-9-._]*@[a-z0-9-._]+$': '^[a-zA-Z0-9-._]*@[a-zA-Z0-9-._]+$',
};
/* eslint-enable max-len */
const UNRECOGNIZED_PROPERTY_REFS = [
'UnrecognizedProperty',
'manifest#/types/UnrecognizedProperty',
@ -31,11 +46,13 @@ export const inner = {};
// use in Firefox only. We shouldn't import these schemas.
export const ignoredSchemas = ['omnibox_internal'];
function stripFlagsFromPattern(value) {
// TODO: Fix these patterns and remove this code.
const matches = FLAG_PATTERN_REGEX.exec(value);
if (matches) {
return matches[1];
function rewritePatternFlags(value) {
if (FLAG_PATTERN_REGEX.test(value)) {
const rewritten = FLAG_PATTERN_REWRITES[value];
if (!rewritten) {
throw new Error(`pattern ${value} must be rewritten`);
}
return rewritten;
}
return value;
}
@ -120,7 +137,7 @@ export function rewriteValue(key, value) {
} else if (key === 'id') {
return undefined;
} else if (key === 'pattern') {
return stripFlagsFromPattern(value);
return rewritePatternFlags(value);
}
return value;
}

Просмотреть файл

@ -1,37 +0,0 @@
import ajv from 'ajv';
import URL from 'url-parse';
import { isRelativeURL, isValidVersionString } from './formats';
import schemaObject from 'schema/imported/manifest.json';
import schemas from './imported';
function isURL(value) {
const url = new URL(value);
return ['http:', 'https:'].includes(url.protocol);
}
function isSecureURL(value) {
const url = new URL(value);
return url.protocol === 'https:';
}
function isStrictRelativeUrl(value) {
return !value.startsWith('//') && isRelativeURL(value);
}
var validator = ajv({
allErrors: true,
errorDataPath: 'property',
jsonPointers: true,
verbose: true,
schemas,
});
validator.addFormat('versionString', isValidVersionString);
validator.addFormat('relativeUrl', isRelativeURL);
validator.addFormat('strictRelativeUrl', isStrictRelativeUrl);
validator.addFormat('url', isURL);
validator.addFormat('secureUrl', isSecureURL);
validator.addFormat('deprecated', () => false);
export default validator.compile(schemaObject);

Просмотреть файл

@ -60,7 +60,7 @@
},
"homepage_url": {
"type": "string",
"format": null,
"format": "ignore",
"preprocess": "localize",
"oneOf": [
{
@ -315,11 +315,11 @@
"anyOf": [
{
"type": "string",
"pattern": "^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$"
"pattern": "^\\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\}$"
},
{
"type": "string",
"pattern": "^[a-z0-9-._]*@[a-z0-9-._]+$"
"pattern": "^[a-zA-Z0-9-._]*@[a-zA-Z0-9-._]+$"
}
]
},

Просмотреть файл

@ -1,15 +0,0 @@
import ajv from 'ajv';
import { isRelativeURL, isValidVersionString } from './formats';
import schemaObject from 'schema/manifest-schema.json';
var validator = ajv({
allErrors: true,
errorDataPath: 'property',
jsonPointers: true,
verbose: true,
});
validator.addFormat('versionString', isValidVersionString);
validator.addFormat('relativeURL', isRelativeURL);
export default validator.compile(schemaObject);

Просмотреть файл

@ -1,224 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://jsonschema.net",
"type": "object",
"properties": {
"applications": {
"type": "object",
"properties": {
"gecko": {
"type": "object",
"properties": {
"id": {
"description": "id is the extension ID. Optional. See https://developer.mozilla.org/Add-ons/Install_Manifests#id",
"pattern": "^{[A-Fa-f0-9]{8}-([A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}}$|^[A-Za-z0-9-._]*\\@[A-Za-z0-9-._]+$",
"type": "string"
},
"strict_min_version": {
"default": "42a1",
"description": "Minimum version of Gecko to support. Defaults to '42a1'. (Requires Gecko 45)",
"type": "string",
"pattern": "^[0-9]{1,3}(\\.[a-z0-9]+)+$"
},
"strict_max_version": {
"default": "*",
"description": "Maximum version of Gecko to support. Defaults to '*'. (Requires Gecko 45)",
"type": "string",
"pattern": "^[0-9]{1,3}(\\.[a-z0-9*]+)+$"
},
"update_url": {
"description": "Link to an add-on update manifest. (Requires Gecko 45)",
"type": "string",
"pattern": "^https://.*$"
}
}
}
}
},
"name": {
"description": "Name of the extension. This is used to identify the extension in the browser's user interface and on sites like addons.mozilla.org.",
"type": "string",
"minLength": 2,
"maxLength": 45
},
"homepage_url": {
"description": "The URL of the homepage for this add-on. The add-on management page will contain a link to this URL.",
"type": "string",
"oneOf": [
{
"type": "string",
"format": "uri"
},
{
"type": "string",
"pattern": "^__MSG_.*?__$"
}
]
},
"icons": {
"description": "Icons to be shown in buttons and the add-on management page.",
"type": "object",
"patternProperties": {
"^[1-9]\\d*$": {
"type": "string"
}
},
"additionalProperties": false
},
"background": {
"description": "This key specifies how background scripts will work.",
"type": "object",
"properties": {
"scripts": {
"type": "array",
"items": {
"format": "relativeURL"
}
},
"page": {
"type": "string",
"format": "relativeURL"
},
"persistent": {
"type": "boolean"
}
},
"additionalProperties": false
},
"manifest_version": {
"description": "This key specifies the version of manifest.json used by this extension. Currently, this must always be 2.",
"minimum": 2,
"maximum": 2,
"type": "integer"
},
"version": {
"$ref": "#/definitions/versionString"
},
"permissions": {
"description": "Permissions allowed for the extension",
"type": "array",
"uniqueItems": true,
"items": {
"anyOf": [
{
"type": "string",
"enum": [
"activeTab",
"alarms",
"bookmarks",
"clipboardWrite",
"contextMenus",
"cookies",
"downloads",
"downloads.open",
"downloads.shelf",
"history",
"idle",
"nativeMessaging",
"notifications",
"storage",
"tabs",
"webNavigation",
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
},
{
"type": "string",
"pattern": "^(https?|file|ftp|app|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+)/.*$"
},
{
"type": "string",
"pattern": "^file:///.*$"
}
]
}
},
"web_accessible_resources": {
"description": "Resources accessible to the extension",
"type": "array",
"items": {
"type": "string"
}
},
"incognito": {
"description": "The behaviour in incognito or private browsing mode",
"type": "string",
"pattern": "^spanning$"
},
"author": {
"description": "Legacy field from Chrome. Use \"developer\" instead.",
"type": "string"
},
"commands": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"suggested_key": {
"type": "object",
"properties": {
"default": {
"$ref": "#/definitions/KeyName"
},
"mac": {
"$ref": "#/definitions/KeyName"
},
"linux": {
"$ref": "#/definitions/KeyName"
},
"windows": {
"$ref": "#/definitions/KeyName"
},
"chromeos": {
"type": "string"
},
"android": {
"type": "string"
},
"ios": {
"type": "string"
},
"additionalProperties": {
"type": "string",
"deprecated": "Unknown platform name"
}
}
},
"description": {
"type": "string"
}
}
}
}
},
"required": [
"name",
"manifest_version",
"version"
],
"definitions": {
"versionString": {
"format": "versionString",
"type": "string",
"description": "Version string must be a string comprising one to four dot-separated integers (0-65535). E.g: 1.2.3."
},
"KeyName": {
"anyOf": [
{
"type": "string",
"pattern": "^\\s*(Alt|Ctrl|Command|MacCtrl)\\s*\\+\\s*(Shift\\s*\\+\\s*)?([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)\\s*$"
},
{
"type": "string",
"pattern": "^\\s*((Alt|Ctrl|Command|MacCtrl)\\s*\\+\\s*)?(Shift\\s*\\+\\s*)?(F[1-9]|F1[0-2])\\s*$"
},
{
"type": "string",
"pattern": "^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$"
}
]
}
}
}

Просмотреть файл

@ -33,8 +33,8 @@
]
},
"homepage_url": {
"format": "ignore",
"type": "string",
"format": null,
"oneOf": [
{
"format": "url"

Просмотреть файл

@ -1,3 +1,38 @@
export default process.env.USE_FIREFOX_SCHEMAS
? require('./firefox-validator').default
: require('./linter-validator').default;
import ajv from 'ajv';
import URL from 'url-parse';
import { isRelativeURL, isValidVersionString } from './formats';
import schemaObject from 'schema/imported/manifest.json';
import schemas from './imported';
function isURL(value) {
const url = new URL(value);
return ['http:', 'https:'].includes(url.protocol);
}
function isSecureURL(value) {
const url = new URL(value);
return url.protocol === 'https:';
}
function isStrictRelativeUrl(value) {
return !value.startsWith('//') && isRelativeURL(value);
}
var validator = ajv({
allErrors: true,
errorDataPath: 'property',
jsonPointers: true,
verbose: true,
schemas,
});
validator.addFormat('versionString', isValidVersionString);
validator.addFormat('relativeUrl', isRelativeURL);
validator.addFormat('strictRelativeUrl', isStrictRelativeUrl);
validator.addFormat('url', isURL);
validator.addFormat('secureUrl', isSecureURL);
validator.addFormat('deprecated', () => false);
validator.addFormat('contentSecurityPolicy', () => true);
validator.addFormat('ignore', () => true);
export default validator.compile(schemaObject);

Просмотреть файл

@ -12,8 +12,7 @@ describe('/background', () => {
validate(manifest);
assertHasMatchingError(validate.errors, {
dataPath: '/background/scripts/0',
// TODO(FxSchema): Switch to just strictRelativeUrl.
message: /should match format "(relativeURL|strictRelativeUrl)"/,
message: /should match format "strictRelativeUrl"/,
});
});
@ -45,8 +44,7 @@ describe('/background', () => {
validate(manifest);
assertHasMatchingError(validate.errors, {
dataPath: '/background/page',
// TODO(FxSchema): Switch to just strictRelativeUrl.
message: /should match format "(relativeURL|strictRelativeUrl)"/,
message: /should match format "strictRelativeUrl"/,
});
});

Просмотреть файл

@ -7,6 +7,7 @@ import request from 'request';
import tar from 'tar';
import {
FLAG_PATTERN_REWRITES,
fetchSchemas,
filterSchemas,
foldSchemas,
@ -126,9 +127,30 @@ describe('firefox schema import', () => {
assert.equal(rewriteValue('type', 'string'), 'string');
});
it('strips flags from patterns', () => {
assert.equal(rewriteValue('pattern', '(?i)^abc$'), '^abc$');
assert.equal(rewriteValue('pattern', '^foo(?i)bar$'), '^foo(?i)bar$');
describe('pattern rewriting', () => {
const originalPattern = '(?i)foo';
before(() => {
FLAG_PATTERN_REWRITES[originalPattern] = 'sup';
});
after(() => {
delete FLAG_PATTERN_REWRITES[originalPattern];
});
it('throws on an unknown pattern with flags', () => {
assert.throws(
() => rewriteValue('pattern', '(?i)^abc$'),
'pattern (?i)^abc$ must be rewritten');
});
it('rewrites known patterns', () => {
assert.equal(rewriteValue('pattern', originalPattern), 'sup');
});
it('does not rewrite unknown patterns without flags', () => {
assert.equal(rewriteValue('pattern', 'abc(?i)def'), 'abc(?i)def');
});
});
it('updates $ref to JSON pointer', () => {

Просмотреть файл

@ -34,8 +34,7 @@ describe('/homepage_url', () => {
validate(manifest);
assertHasMatchingError(validate.errors, {
dataPath: '/homepage_url',
// TODO(FxSchema): Switch to just '... "url"'.
message: /should match format "ur[il]"/,
message: /should match format "url"/,
});
assertHasMatchingError(validate.errors, {
message: 'should match pattern "^__MSG_.*?__$"',

Просмотреть файл

@ -20,13 +20,7 @@ describe('/incognito', () => {
validate(manifest);
assertHasMatchingError(validate.errors, {
dataPath: '/incognito',
message: new RegExp(
'(' +
// TODO(FxSchema): Switch to just this string.
'should be equal to one of the allowed values' +
'|' +
'should match pattern "\\^spanning\\$"' +
')'),
message: 'should be equal to one of the allowed values',
});
});