1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

Add Uri matching

This commit is contained in:
Matt Gibson
2025-03-13 18:19:51 -07:00
parent 47bb10fec1
commit ac544c4b31
4 changed files with 130 additions and 5 deletions

View File

@@ -17,6 +17,8 @@ export const AstNodeTypeNames = [
"inTrash",
"isFavorite",
"type",
"website",
"websiteMatch",
] as const;
export type AstNodeType = (typeof AstNodeTypeNames)[number];
export type AstNode =
@@ -37,7 +39,9 @@ export type AstNode =
| InMyVault
| InTrash
| IsFavorite
| TypeFilter;
| TypeFilter
| WebsiteFilter
| WebsiteMatchFilter;
type AstNodeBase = {
d: object[];
@@ -203,3 +207,22 @@ export type TypeFilter = AstNodeBase & {
export function isTypeFilter(x: AstNode): x is TypeFilter {
return x.type === "type";
}
export type WebsiteFilter = AstNodeBase & {
type: "website";
website: string;
};
export function isWebsiteFilter(x: AstNode): x is WebsiteFilter {
return x.type === "website";
}
export type WebsiteMatchFilter = AstNodeBase & {
type: "websiteMatch";
website: string;
matchType: string;
};
export function isWebsiteMatchFilter(x: AstNode): x is WebsiteMatchFilter {
return x.type === "websiteMatch";
}

View File

@@ -16,6 +16,7 @@ let lexer = moo.compile({
func_in: 'in:',
func_is: 'is:',
func_type: 'type:',
func_website: 'website:',
// function parameter separator
access: ':',
// string match, includes quoted strings with escaped quotes and backslashes
@@ -56,6 +57,7 @@ TERM ->
| %func_in "collection" %access %string {% function(d) { const start = d[0].offset; const end = d[3].offset + d[3].value.length; return { type: 'inCollection', collection: d[3].value, d: d, start, end, length: end - start + 1 } } %}
# only items assigned to a specified organization
| %func_in "org" %access %string {% function(d) { const start = d[0].offset; const end = d[3].offset + d[3].value.length; return { type: 'inOrg', org: d[3].value, d: d, start, end, length: end - start + 1 } } %}
# only items in personal vault
| %func_in "my_vault" {% function(d) { const start = d[0].offset; const length = 11; return { type: 'inMyVault', d: d, start, end: start + length, length } } %}
# only items in trash
| %func_in "trash" {% function(d) { const start = d[0].offset; const length = 8; return { type: 'inTrash', d: d, start, end: start + length, length } } %}
@@ -63,6 +65,10 @@ TERM ->
| %func_is "favorite" {% function(d) { const start = d[0].offset; const length = 11; return { type: 'isFavorite', d: d, start, end: start + length, length } } %}
# only items of given type type
| %func_type %string {% function(d) { const start = d[0].offset; const end = d[1].offset + d[1].value.length; return { type: 'type', d:d, cipherType: d[1].value, start, end, length: end - start + 1 } } %}
# only items with a specified website
| %func_website %string {% function(d) { const start = d[0].offset; const end = d[1].offset + d[1].value.length; return { type: 'website', d: d, website: d[1].value, start, end, length: end - start + 1 } } %}
# only items with a specified website and a given match pattern
| %func_website %string %access %string {% function(d) { const start = d[0].offset; const end = d[3].offset + d[3].value.length; return { type: 'websiteMatch', d: d, website: d[1].value, matchType: d[3].value, start, end, length: end - start + 1 } } %}
# Boolean NOT operator
| %NOT _ PARENTHESES {% function(d) { const start = d[0].offset; return { type: 'not', value: d[2], d: d, start, end: d[2].end, length: d[2].end - d[0].offset + 1 } } %}

View File

@@ -15,6 +15,7 @@ declare var func_has: any;
declare var func_in: any;
declare var func_is: any;
declare var func_type: any;
declare var func_website: any;
declare var NOT: any;
declare var WS: any;
@@ -34,6 +35,7 @@ let lexer = moo.compile({
func_in: "in:",
func_is: "is:",
func_type: "type:",
func_website: "website:",
// function parameter separator
access: ":",
// string match, includes quoted strings with escaped quotes and backslashes
@@ -301,6 +303,40 @@ const grammar: Grammar = {
return { type: "type", d: d, cipherType: d[1].value, start, end, length: end - start + 1 };
},
},
{
name: "TERM",
symbols: [
lexer.has("func_website") ? { type: "func_website" } : func_website,
lexer.has("string") ? { type: "string" } : string,
],
postprocess: function (d) {
const start = d[0].offset;
const end = d[1].offset + d[1].value.length;
return { type: "website", d: d, website: d[1].value, start, end, length: end - start + 1 };
},
},
{
name: "TERM",
symbols: [
lexer.has("func_website") ? { type: "func_website" } : func_website,
lexer.has("string") ? { type: "string" } : string,
lexer.has("access") ? { type: "access" } : access,
lexer.has("string") ? { type: "string" } : string,
],
postprocess: function (d) {
const start = d[0].offset;
const end = d[3].offset + d[3].value.length;
return {
type: "websiteMatch",
d: d,
website: d[1].value,
matchType: d[3].value,
start,
end,
length: end - start + 1,
};
},
},
{
name: "TERM",
symbols: [lexer.has("NOT") ? { type: "NOT" } : NOT, "_", "PARENTHESES"],

View File

@@ -1,5 +1,6 @@
import { Parser, Grammar } from "nearley";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { Utils } from "../../platform/misc/utils";
import { CardLinkedId, CipherType, FieldType, LinkedIdType, LoginLinkedId } from "../enums";
import { CipherView } from "../models/view/cipher.view";
@@ -23,6 +24,8 @@ import {
isSearch,
isTerm,
isTypeFilter,
isWebsiteFilter,
isWebsiteMatchFilter,
} from "./ast";
import grammar from "./bitwarden-query-grammar";
import { ProcessInstructions } from "./query.types";
@@ -315,10 +318,48 @@ function handleNode(node: AstNode): ProcessInstructions {
return {
filter: (context) => ({
...context,
ciphers: context.ciphers.filter(
(cipher) =>
typeTest.test(CipherType[cipher.type]) ||
CipherType[node.cipherType as any] === CipherType[cipher.type],
ciphers: context.ciphers.filter((cipher) =>
matchEnum(CipherType, cipher.type, typeTest, node.cipherType),
),
}),
sections: [
{
start: node.start,
end: node.end,
type: node.type,
},
],
};
} else if (isWebsiteFilter(node)) {
const websiteTest = termToRegexTest(node.website);
return {
filter: (context) => ({
...context,
ciphers: context.ciphers.filter((cipher) =>
cipher?.login?.uris?.some((uri) => websiteTest.test(uri.uri)),
),
}),
sections: [
{
start: node.start,
end: node.end,
type: node.type,
},
],
};
} else if (isWebsiteMatchFilter(node)) {
const websiteTest = termToRegexTest(node.website);
const matchTest = fieldNameToRegexTest(node.matchType);
const matchTypes = Object.keys(UriMatchStrategy)
.filter((key) => matchTest.test(key))
.map((key) => UriMatchStrategy[key as keyof typeof UriMatchStrategy]);
return {
filter: (context) => ({
...context,
ciphers: context.ciphers.filter((cipher) =>
cipher?.login?.uris?.some(
(uri) => matchTypes.includes(uri.match) && websiteTest.test(uri.uri),
),
),
}),
sections: [
@@ -334,6 +375,25 @@ function handleNode(node: AstNode): ProcessInstructions {
}
}
/**
* Match a string against an enum value. The matching string is sent in twice to match the enum in both directions,
* number -> string and string -> number.
*
* @param enumObj The Enum type
* @param cipherVal The existing value on a cipher to test for a match
* @param valTest The regex test to apply to cipherVal
* @param targetValue The raw value to test against the cipherVal
* @returns
*/
function matchEnum(
enumObj: { [name: string]: any },
cipherVal: string | number,
valTest: RegExp,
targetValue: string,
) {
return valTest.test(enumObj[cipherVal]) || enumObj[targetValue] === enumObj[cipherVal];
}
function hasTerm(cipher: CipherView, termTest: RegExp, fieldTest: RegExp = /.*/i): boolean {
const foundValues = fieldValues(cipher, fieldTest);