mirror of
https://github.com/stashapp/stash.git
synced 2025-12-15 12:52:38 +01:00
Validate tagger blacklist entries (#5497)
* Don't let invalid tagger regex crash UI * Validate blacklist entries and show errors
This commit is contained in:
parent
6c5bf5f052
commit
f81202660c
3 changed files with 120 additions and 73 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
import React, { useRef, useContext } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -14,6 +14,102 @@ import { Icon } from "src/components/Shared/Icon";
|
||||||
import { ParseMode, TagOperation } from "../constants";
|
import { ParseMode, TagOperation } from "../constants";
|
||||||
import { TaggerStateContext } from "../context";
|
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 {
|
interface IConfigProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -21,33 +117,6 @@ interface IConfigProps {
|
||||||
const Config: React.FC<IConfigProps> = ({ show }) => {
|
const Config: React.FC<IConfigProps> = ({ show }) => {
|
||||||
const { config, setConfig } = useContext(TaggerStateContext);
|
const { config, setConfig } = useContext(TaggerStateContext);
|
||||||
const intl = useIntl();
|
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 (
|
return (
|
||||||
<Collapse in={show}>
|
<Collapse in={show}>
|
||||||
|
|
@ -198,47 +267,10 @@ const Config: React.FC<IConfigProps> = ({ show }) => {
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<h5>
|
<Blacklist
|
||||||
<FormattedMessage id="component_tagger.config.blacklist_label" />
|
list={config.blacklist}
|
||||||
</h5>
|
setList={(blacklist) => setConfig({ ...config, blacklist })}
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,17 @@ export function prepareQueryString(
|
||||||
mode: ParseMode,
|
mode: ParseMode,
|
||||||
blacklist: string[]
|
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") {
|
if ((mode === "auto" && scene.date && scene.studio) || mode === "metadata") {
|
||||||
let str = [
|
let str = [
|
||||||
scene.date,
|
scene.date,
|
||||||
|
|
@ -92,8 +103,8 @@ export function prepareQueryString(
|
||||||
]
|
]
|
||||||
.filter((s) => s !== "")
|
.filter((s) => s !== "")
|
||||||
.join(" ");
|
.join(" ");
|
||||||
blacklist.forEach((b) => {
|
regexs.forEach((re) => {
|
||||||
str = str.replace(new RegExp(b, "gi"), " ");
|
str = str.replace(re, " ");
|
||||||
});
|
});
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
@ -106,8 +117,9 @@ export function prepareQueryString(
|
||||||
} else if (mode === "dir" && paths.length) {
|
} else if (mode === "dir" && paths.length) {
|
||||||
s = paths[paths.length - 1];
|
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);
|
s = parseDate(s);
|
||||||
return s.replace(/\./g, " ").replace(/ +/g, " ");
|
return s.replace(/\./g, " ").replace(/ +/g, " ");
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,9 @@
|
||||||
"active_instance": "Active stash-box instance:",
|
"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_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",
|
"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_desc": "Immediately mark the scene as Organized after the Save button is clicked.",
|
||||||
"mark_organized_label": "Mark as Organized on save",
|
"mark_organized_label": "Mark as Organized on save",
|
||||||
"query_mode_auto": "Auto",
|
"query_mode_auto": "Auto",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue