Validate tagger blacklist entries (#5497)

* Don't let invalid tagger regex crash UI
* Validate blacklist entries and show errors
This commit is contained in:
WithoutPants 2024-11-22 08:27:41 +11:00 committed by GitHub
parent 6c5bf5f052
commit f81202660c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 120 additions and 73 deletions

View file

@ -1,5 +1,5 @@
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import React, { useRef, useContext } from "react";
import React, { useContext, useState } from "react";
import {
Badge,
Button,
@ -14,6 +14,102 @@ import { Icon } from "src/components/Shared/Icon";
import { ParseMode, TagOperation } from "../constants";
import { TaggerStateContext } from "../context";
const Blacklist: React.FC<{
list: string[];
setList: (blacklist: string[]) => void;
}> = ({ list, setList }) => {
const intl = useIntl();
const [currentValue, setCurrentValue] = useState("");
const [error, setError] = useState<string>();
function addBlacklistItem() {
if (!currentValue) return;
// don't add duplicate items
if (list.includes(currentValue)) {
setError(
intl.formatMessage({
id: "component_tagger.config.errors.blacklist_duplicate",
})
);
return;
}
// validate regex
try {
new RegExp(currentValue);
} catch (e) {
setError((e as SyntaxError).message);
return;
}
setList([...list, currentValue]);
setCurrentValue("");
}
function removeBlacklistItem(index: number) {
const newBlacklist = [...list];
newBlacklist.splice(index, 1);
setList(newBlacklist);
}
return (
<div>
<h5>
<FormattedMessage id="component_tagger.config.blacklist_label" />
</h5>
<Form.Group>
<InputGroup hasValidation>
<Form.Control
className="text-input"
value={currentValue}
onChange={(e) => {
setCurrentValue(e.currentTarget.value);
setError(undefined);
}}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
addBlacklistItem();
e.preventDefault();
}
}}
isInvalid={!!error}
/>
<InputGroup.Append>
<Button onClick={() => addBlacklistItem()}>
<FormattedMessage id="actions.add" />
</Button>
</InputGroup.Append>
<Form.Control.Feedback type="invalid">{error}</Form.Control.Feedback>
</InputGroup>
</Form.Group>
<div>
{intl.formatMessage(
{ id: "component_tagger.config.blacklist_desc" },
{ chars_require_escape: <code>[\^$.|?*+()</code> }
)}
</div>
{list.map((item, index) => (
<Badge
className="tag-item d-inline-block"
variant="secondary"
key={item}
>
{item.toString()}
<Button
className="minimal ml-2"
onClick={() => removeBlacklistItem(index)}
>
<Icon icon={faTimes} />
</Button>
</Badge>
))}
</div>
);
};
interface IConfigProps {
show: boolean;
}
@ -21,33 +117,6 @@ interface IConfigProps {
const Config: React.FC<IConfigProps> = ({ show }) => {
const { config, setConfig } = useContext(TaggerStateContext);
const intl = useIntl();
const blacklistRef = useRef<HTMLInputElement | null>(null);
function addBlacklistItem() {
if (!blacklistRef.current) return;
const input = blacklistRef.current.value;
if (!input) return;
// don't add duplicate items
if (!config.blacklist.includes(input)) {
setConfig({
...config,
blacklist: [...config.blacklist, input],
});
}
blacklistRef.current.value = "";
}
function removeBlacklistItem(index: number) {
const newBlacklist = [...config.blacklist];
newBlacklist.splice(index, 1);
setConfig({
...config,
blacklist: newBlacklist,
});
}
return (
<Collapse in={show}>
@ -198,47 +267,10 @@ const Config: React.FC<IConfigProps> = ({ show }) => {
</Form.Group>
</Form>
<div className="col-md-6">
<h5>
<FormattedMessage id="component_tagger.config.blacklist_label" />
</h5>
<InputGroup>
<Form.Control
className="text-input"
ref={blacklistRef}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
addBlacklistItem();
e.preventDefault();
}
}}
/>
<InputGroup.Append>
<Button onClick={() => addBlacklistItem()}>
<FormattedMessage id="actions.add" />
</Button>
</InputGroup.Append>
</InputGroup>
<div>
{intl.formatMessage(
{ id: "component_tagger.config.blacklist_desc" },
{ chars_require_escape: <code>[\^$.|?*+()</code> }
)}
</div>
{config.blacklist.map((item, index) => (
<Badge
className="tag-item d-inline-block"
variant="secondary"
key={item}
>
{item.toString()}
<Button
className="minimal ml-2"
onClick={() => removeBlacklistItem(index)}
>
<Icon icon={faTimes} />
</Button>
</Badge>
))}
<Blacklist
list={config.blacklist}
setList={(blacklist) => setConfig({ ...config, blacklist })}
/>
</div>
</div>
</Card>

View file

@ -83,6 +83,17 @@ export function prepareQueryString(
mode: ParseMode,
blacklist: string[]
) {
const regexs = blacklist
.map((b) => {
try {
return new RegExp(b, "gi");
} catch {
// ignore
return null;
}
})
.filter((r) => r !== null) as RegExp[];
if ((mode === "auto" && scene.date && scene.studio) || mode === "metadata") {
let str = [
scene.date,
@ -92,8 +103,8 @@ export function prepareQueryString(
]
.filter((s) => s !== "")
.join(" ");
blacklist.forEach((b) => {
str = str.replace(new RegExp(b, "gi"), " ");
regexs.forEach((re) => {
str = str.replace(re, " ");
});
return str;
}
@ -106,8 +117,9 @@ export function prepareQueryString(
} else if (mode === "dir" && paths.length) {
s = paths[paths.length - 1];
}
blacklist.forEach((b) => {
s = s.replace(new RegExp(b, "gi"), " ");
regexs.forEach((re) => {
s = s.replace(re, " ");
});
s = parseDate(s);
return s.replace(/\./g, " ").replace(/ +/g, " ");

View file

@ -173,6 +173,9 @@
"active_instance": "Active stash-box instance:",
"blacklist_desc": "Blacklist items are excluded from queries. Note that they are regular expressions and also case-insensitive. Certain characters must be escaped with a backslash: {chars_require_escape}",
"blacklist_label": "Blacklist",
"errors": {
"blacklist_duplicate": "Duplicate blacklist item"
},
"mark_organized_desc": "Immediately mark the scene as Organized after the Save button is clicked.",
"mark_organized_label": "Mark as Organized on save",
"query_mode_auto": "Auto",