Skip to content

Commit

Permalink
Validate tagger blacklist entries (#5497)
Browse files Browse the repository at this point in the history
* Don't let invalid tagger regex crash UI
* Validate blacklist entries and show errors
  • Loading branch information
WithoutPants authored Nov 21, 2024
1 parent 6c5bf5f commit f812026
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 68 deletions.
160 changes: 96 additions & 64 deletions ui/v2.5/src/components/Tagger/scenes/Config.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -14,41 +14,110 @@ import { Icon } from "src/components/Shared/Icon";
import { ParseMode, TagOperation } from "../constants";
import { TaggerStateContext } from "../context";

interface IConfigProps {
show: boolean;
}

const Config: React.FC<IConfigProps> = ({ show }) => {
const { config, setConfig } = useContext(TaggerStateContext);
const Blacklist: React.FC<{
list: string[];
setList: (blacklist: string[]) => void;
}> = ({ list, setList }) => {
const intl = useIntl();
const blacklistRef = useRef<HTMLInputElement | null>(null);

function addBlacklistItem() {
if (!blacklistRef.current) return;
const [currentValue, setCurrentValue] = useState("");
const [error, setError] = useState<string>();

const input = blacklistRef.current.value;
if (!input) return;
function addBlacklistItem() {
if (!currentValue) return;

// don't add duplicate items
if (!config.blacklist.includes(input)) {
setConfig({
...config,
blacklist: [...config.blacklist, input],
});
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;
}

blacklistRef.current.value = "";
setList([...list, currentValue]);

setCurrentValue("");
}

function removeBlacklistItem(index: number) {
const newBlacklist = [...config.blacklist];
const newBlacklist = [...list];
newBlacklist.splice(index, 1);
setConfig({
...config,
blacklist: newBlacklist,
});
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;
}

const Config: React.FC<IConfigProps> = ({ show }) => {
const { config, setConfig } = useContext(TaggerStateContext);
const intl = useIntl();

return (
<Collapse in={show}>
<Card>
Expand Down Expand Up @@ -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>
Expand Down
20 changes: 16 additions & 4 deletions ui/v2.5/src/components/Tagger/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -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, " ");
Expand Down
3 changes: 3 additions & 0 deletions ui/v2.5/src/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit f812026

Please sign in to comment.