mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Fix URL encoding (#2899)
* Fix URL encoding * Optimize nullable criterion encoding
This commit is contained in:
@@ -107,7 +107,7 @@ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
|||||||
|
|
||||||
const ret = new ListFilterModel(mode);
|
const ret = new ListFilterModel(mode);
|
||||||
ret.currentPage = 1;
|
ret.currentPage = 1;
|
||||||
ret.configureFromQueryParameters(JSON.parse(filterJSON));
|
ret.configureFromJSON(filterJSON);
|
||||||
ret.randomSeed = -1;
|
ret.randomSeed = -1;
|
||||||
return ret;
|
return ret;
|
||||||
}, [data?.findSavedFilter]);
|
}, [data?.findSavedFilter]);
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
|||||||
|
|
||||||
async function onSaveFilter(name: string, id?: string) {
|
async function onSaveFilter(name: string, id?: string) {
|
||||||
const filterCopy = filter.clone();
|
const filterCopy = filter.clone();
|
||||||
filterCopy.currentPage = 1;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -76,7 +75,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
|||||||
id,
|
id,
|
||||||
mode: filter.mode,
|
mode: filter.mode,
|
||||||
name,
|
name,
|
||||||
filter: JSON.stringify(filterCopy.getSavedQueryParameters()),
|
filter: filterCopy.makeSavedFilterJSON(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -136,7 +135,6 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
|||||||
|
|
||||||
async function onSetDefaultFilter() {
|
async function onSetDefaultFilter() {
|
||||||
const filterCopy = filter.clone();
|
const filterCopy = filter.clone();
|
||||||
filterCopy.currentPage = 1;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -145,7 +143,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
mode: filter.mode,
|
mode: filter.mode,
|
||||||
filter: JSON.stringify(filterCopy.getSavedQueryParameters()),
|
filter: filterCopy.makeSavedFilterJSON(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -165,7 +163,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
|||||||
function filterClicked(f: SavedFilterDataFragment) {
|
function filterClicked(f: SavedFilterDataFragment) {
|
||||||
const newFilter = filter.clone();
|
const newFilter = filter.clone();
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
newFilter.configureFromQueryParameters(JSON.parse(f.filter));
|
newFilter.configureFromJSON(f.filter);
|
||||||
// #1507 - reset random seed when loaded
|
// #1507 - reset random seed when loaded
|
||||||
newFilter.randomSeed = -1;
|
newFilter.randomSeed = -1;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ After migrating, please run a scan on your entire library to populate missing da
|
|||||||
* Added release notes dialog. ([#2726](https://github.com/stashapp/stash/pull/2726))
|
* Added release notes dialog. ([#2726](https://github.com/stashapp/stash/pull/2726))
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Encode reserved characters in query URLs. ([#2899](https://github.com/stashapp/stash/pull/2899))
|
||||||
* Object titles are now displayed as the file basename if the title is not explicitly set. The `Don't include file extension as part of the title` scan flag is no longer supported.
|
* Object titles are now displayed as the file basename if the title is not explicitly set. The `Don't include file extension as part of the title` scan flag is no longer supported.
|
||||||
* `Set name, date, details from embedded file metadata` scan flag is no longer supported. This functionality may be implemented as a built-in scraper in the future.
|
* `Set name, date, details from embedded file metadata` scan flag is no longer supported. This functionality may be implemented as a built-in scraper in the future.
|
||||||
* Moved Changelogs to Settings page. ([#2726](https://github.com/stashapp/stash/pull/2726))
|
* Moved Changelogs to Settings page. ([#2726](https://github.com/stashapp/stash/pull/2726))
|
||||||
|
|||||||
@@ -572,13 +572,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
const defaultSort = options.defaultSort ?? filterOptions.defaultSortBy;
|
const defaultSort = options.defaultSort ?? filterOptions.defaultSortBy;
|
||||||
const defaultDisplayMode = filterOptions.displayModeOptions[0];
|
const defaultDisplayMode = filterOptions.displayModeOptions[0];
|
||||||
const createNewFilter = useCallback(() => {
|
const createNewFilter = useCallback(() => {
|
||||||
return new ListFilterModel(
|
const filter = new ListFilterModel(
|
||||||
options.filterMode,
|
options.filterMode,
|
||||||
queryString.parse(history.location.search),
|
|
||||||
defaultSort,
|
defaultSort,
|
||||||
defaultDisplayMode,
|
defaultDisplayMode,
|
||||||
options.defaultZoomIndex
|
options.defaultZoomIndex
|
||||||
);
|
);
|
||||||
|
filter.configureFromQueryString(history.location.search);
|
||||||
|
return filter;
|
||||||
}, [
|
}, [
|
||||||
options.filterMode,
|
options.filterMode,
|
||||||
history,
|
history,
|
||||||
@@ -654,9 +655,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
if (defaultFilter?.findDefaultFilter) {
|
if (defaultFilter?.findDefaultFilter) {
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
try {
|
try {
|
||||||
newFilter.configureFromQueryParameters(
|
newFilter.configureFromJSON(defaultFilter.findDefaultFilter.filter);
|
||||||
JSON.parse(defaultFilter.findDefaultFilter.filter)
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
// ignore
|
// ignore
|
||||||
@@ -713,9 +712,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
|
|
||||||
setFilter((prevFilter) => {
|
setFilter((prevFilter) => {
|
||||||
let newFilter = prevFilter.clone();
|
let newFilter = prevFilter.clone();
|
||||||
newFilter.configureFromQueryParameters(
|
newFilter.configureFromQueryString(location.search);
|
||||||
queryString.parse(location.search)
|
|
||||||
);
|
|
||||||
if (!isEqual(newFilter, prevFilter)) {
|
if (!isEqual(newFilter, prevFilter)) {
|
||||||
return newFilter;
|
return newFilter;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
import {
|
import {
|
||||||
CriterionType,
|
CriterionType,
|
||||||
encodeILabeledId,
|
|
||||||
encodeLabel,
|
|
||||||
IHierarchicalLabelValue,
|
IHierarchicalLabelValue,
|
||||||
ILabeledId,
|
ILabeledId,
|
||||||
ILabeledValue,
|
ILabeledValue,
|
||||||
@@ -54,7 +52,14 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||||||
|
|
||||||
public criterionOption: CriterionOption;
|
public criterionOption: CriterionOption;
|
||||||
public modifier: CriterionModifier;
|
public modifier: CriterionModifier;
|
||||||
public value: V;
|
|
||||||
|
protected _value!: V;
|
||||||
|
public get value(): V {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
public set value(newValue: V) {
|
||||||
|
this._value = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract getLabelValue(): string;
|
public abstract getLabelValue(): string;
|
||||||
|
|
||||||
@@ -97,24 +102,23 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||||||
return `${this.criterionOption.parameterName}-${this.modifier.toString()}`; // TODO add values?
|
return `${this.criterionOption.parameterName}-${this.modifier.toString()}`; // TODO add values?
|
||||||
}
|
}
|
||||||
|
|
||||||
public encodeValue(): V {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public decodeValue(value: V) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON() {
|
public toJSON() {
|
||||||
const encodedCriterion = {
|
let encodedCriterion;
|
||||||
type: this.criterionOption.type,
|
if (
|
||||||
// #394 - the presence of a # symbol results in the query URL being
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
// malformed. We could set encode: true in the queryString.stringify
|
this.modifier === CriterionModifier.NotNull
|
||||||
// call below, but this results in a URL that gets pretty long and ugly.
|
) {
|
||||||
// Instead, we'll encode the criteria values.
|
encodedCriterion = {
|
||||||
value: this.encodeValue(),
|
type: this.criterionOption.type,
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
encodedCriterion = {
|
||||||
|
type: this.criterionOption.type,
|
||||||
|
value: this.value,
|
||||||
|
modifier: this.modifier,
|
||||||
|
};
|
||||||
|
}
|
||||||
return JSON.stringify(encodedCriterion);
|
return JSON.stringify(encodedCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,32 +211,13 @@ export function createStringCriterionOption(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StringCriterion extends Criterion<string> {
|
export class StringCriterion extends Criterion<string> {
|
||||||
public getLabelValue() {
|
|
||||||
let ret = this.value;
|
|
||||||
ret = StringCriterion.unreplaceSpecialCharacter(ret, "&");
|
|
||||||
ret = StringCriterion.unreplaceSpecialCharacter(ret, "+");
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public encodeValue() {
|
|
||||||
// replace certain characters
|
|
||||||
let ret = this.value;
|
|
||||||
ret = StringCriterion.replaceSpecialCharacter(ret, "&");
|
|
||||||
ret = StringCriterion.replaceSpecialCharacter(ret, "+");
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static replaceSpecialCharacter(str: string, c: string) {
|
|
||||||
return str.replaceAll(c, encodeURIComponent(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unreplaceSpecialCharacter(str: string, c: string) {
|
|
||||||
return str.replaceAll(encodeURIComponent(c), c);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, "");
|
super(type, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLabelValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryStringCriterionOption extends CriterionOption {
|
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||||
@@ -337,37 +322,39 @@ export function createNumberCriterionOption(value: CriterionType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NumberCriterion extends Criterion<INumberValue> {
|
export class NumberCriterion extends Criterion<INumberValue> {
|
||||||
private getValue() {
|
public get value(): INumberValue {
|
||||||
// backwards compatibility - if this.value is a number, use that
|
return this._value;
|
||||||
if (typeof this.value !== "object") {
|
|
||||||
return this.value as number;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value.value;
|
|
||||||
}
|
}
|
||||||
|
public set value(newValue: number | INumberValue) {
|
||||||
public encodeValue() {
|
// backwards compatibility - if this.value is a number, use that
|
||||||
return {
|
if (typeof newValue !== "object") {
|
||||||
value: this.getValue(),
|
this._value = {
|
||||||
value2: this.value.value2,
|
value: newValue,
|
||||||
};
|
value2: undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this._value = newValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
// backwards compatibility - if this.value is a number, use that
|
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.getValue(),
|
value: this.value.value,
|
||||||
value2: this.value.value2,
|
value2: this.value.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLabelValue() {
|
public getLabelValue() {
|
||||||
const value = this.getValue();
|
const { value, value2 } = this.value;
|
||||||
return this.modifier === CriterionModifier.Between ||
|
if (
|
||||||
|
this.modifier === CriterionModifier.Between ||
|
||||||
this.modifier === CriterionModifier.NotBetween
|
this.modifier === CriterionModifier.NotBetween
|
||||||
? `${value}, ${this.value.value2 ?? 0}`
|
) {
|
||||||
: `${value}`;
|
return `${value}, ${value2 ?? 0}`;
|
||||||
|
} else {
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
@@ -417,36 +404,12 @@ export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public encodeValue() {
|
|
||||||
return this.value.map((o) => {
|
|
||||||
return encodeILabeledId(o);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, []);
|
super(type, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
|
export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
|
||||||
public encodeValue() {
|
|
||||||
return {
|
|
||||||
items: this.value.items.map((o) => {
|
|
||||||
return encodeILabeledId(o);
|
|
||||||
}),
|
|
||||||
depth: this.value.depth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
public decodeValue(value: any) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
this.value.items = value;
|
|
||||||
} else {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toCriterionInput(): HierarchicalMultiCriterionInput {
|
protected toCriterionInput(): HierarchicalMultiCriterionInput {
|
||||||
return {
|
return {
|
||||||
value: (this.value.items ?? []).map((v) => v.id),
|
value: (this.value.items ?? []).map((v) => v.id),
|
||||||
@@ -456,9 +419,7 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getLabelValue(): string {
|
public getLabelValue(): string {
|
||||||
const labels = decodeURI(
|
const labels = (this.value.items ?? []).map((v) => v.label).join(", ");
|
||||||
(this.value.items ?? []).map((v) => encodeLabel(v.label)).join(", ")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.value.depth === 0) {
|
if (this.value.depth === 0) {
|
||||||
return labels;
|
return labels;
|
||||||
@@ -509,13 +470,6 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
|||||||
super(type, { value: 0, value2: undefined });
|
super(type, { value: 0, value2: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
public encodeValue() {
|
|
||||||
return {
|
|
||||||
value: this.value.value,
|
|
||||||
value2: this.value.value2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import queryString, { ParsedQuery } from "query-string";
|
import queryString, { ParsedQuery } from "query-string";
|
||||||
|
import clone from "lodash-es/clone";
|
||||||
import {
|
import {
|
||||||
FilterMode,
|
FilterMode,
|
||||||
FindFilterType,
|
FindFilterType,
|
||||||
@@ -42,26 +43,24 @@ export class ListFilterModel {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
mode: FilterMode,
|
mode: FilterMode,
|
||||||
rawParms?: ParsedQuery<string>,
|
|
||||||
defaultSort?: string,
|
defaultSort?: string,
|
||||||
defaultDisplayMode?: DisplayMode,
|
defaultDisplayMode?: DisplayMode,
|
||||||
defaultZoomIndex?: number
|
defaultZoomIndex?: number
|
||||||
) {
|
) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
const params = rawParms as IQueryParameters;
|
|
||||||
this.sortBy = defaultSort;
|
this.sortBy = defaultSort;
|
||||||
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
|
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
|
||||||
if (defaultZoomIndex !== undefined) {
|
if (defaultZoomIndex !== undefined) {
|
||||||
this.defaultZoomIndex = defaultZoomIndex;
|
this.defaultZoomIndex = defaultZoomIndex;
|
||||||
this.zoomIndex = defaultZoomIndex;
|
this.zoomIndex = defaultZoomIndex;
|
||||||
}
|
}
|
||||||
if (params) this.configureFromQueryParameters(params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clone() {
|
public clone() {
|
||||||
return Object.assign(new ListFilterModel(this.mode), this);
|
return Object.assign(new ListFilterModel(this.mode), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does not decode any URL-encoding in parameters
|
||||||
public configureFromQueryParameters(params: IQueryParameters) {
|
public configureFromQueryParameters(params: IQueryParameters) {
|
||||||
if (params.sortby !== undefined) {
|
if (params.sortby !== undefined) {
|
||||||
this.sortBy = params.sortby;
|
this.sortBy = params.sortby;
|
||||||
@@ -102,20 +101,15 @@ export class ListFilterModel {
|
|||||||
|
|
||||||
this.criteria = [];
|
this.criteria = [];
|
||||||
if (params.c !== undefined) {
|
if (params.c !== undefined) {
|
||||||
let jsonParameters: string[];
|
params.c.forEach((jsonString) => {
|
||||||
if (params.c instanceof Array) {
|
|
||||||
jsonParameters = params.c;
|
|
||||||
} else {
|
|
||||||
jsonParameters = [params.c];
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonParameters.forEach((jsonString) => {
|
|
||||||
try {
|
try {
|
||||||
const encodedCriterion = JSON.parse(jsonString);
|
const encodedCriterion = JSON.parse(jsonString);
|
||||||
const criterion = makeCriteria(encodedCriterion.type);
|
const criterion = makeCriteria(encodedCriterion.type);
|
||||||
// it's possible that we have unsupported criteria. Just skip if so.
|
// it's possible that we have unsupported criteria. Just skip if so.
|
||||||
if (criterion) {
|
if (criterion) {
|
||||||
criterion.decodeValue(encodedCriterion.value);
|
if (encodedCriterion.value !== undefined) {
|
||||||
|
criterion.value = encodedCriterion.value;
|
||||||
|
}
|
||||||
criterion.modifier = encodedCriterion.modifier;
|
criterion.modifier = encodedCriterion.modifier;
|
||||||
this.criteria.push(criterion);
|
this.criteria.push(criterion);
|
||||||
}
|
}
|
||||||
@@ -127,6 +121,49 @@ export class ListFilterModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static decodeQueryParameters(
|
||||||
|
parsedQuery: ParsedQuery<string>
|
||||||
|
): IQueryParameters {
|
||||||
|
const params = clone(parsedQuery);
|
||||||
|
if (params.q) {
|
||||||
|
let searchTerm: string;
|
||||||
|
if (params.q instanceof Array) {
|
||||||
|
searchTerm = params.q[0];
|
||||||
|
} else {
|
||||||
|
searchTerm = params.q;
|
||||||
|
}
|
||||||
|
params.q = decodeURIComponent(searchTerm);
|
||||||
|
}
|
||||||
|
if (params.c !== undefined) {
|
||||||
|
let jsonParameters: string[];
|
||||||
|
if (params.c instanceof Array) {
|
||||||
|
jsonParameters = params.c;
|
||||||
|
} else {
|
||||||
|
jsonParameters = [params.c!];
|
||||||
|
}
|
||||||
|
params.c = jsonParameters.map((jsonString) => {
|
||||||
|
let decodedJson = jsonString;
|
||||||
|
// replace () back to {}
|
||||||
|
decodedJson = decodedJson.replaceAll("(", "{");
|
||||||
|
decodedJson = decodedJson.replaceAll(")", "}");
|
||||||
|
// decode all other characters
|
||||||
|
decodedJson = decodeURIComponent(decodedJson);
|
||||||
|
return decodedJson;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public configureFromQueryString(query: string) {
|
||||||
|
const parsed = queryString.parse(query, { decode: false });
|
||||||
|
const decoded = ListFilterModel.decodeQueryParameters(parsed);
|
||||||
|
this.configureFromQueryParameters(decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public configureFromJSON(json: string) {
|
||||||
|
this.configureFromQueryParameters(JSON.parse(json));
|
||||||
|
}
|
||||||
|
|
||||||
private setRandomSeed() {
|
private setRandomSeed() {
|
||||||
if (this.sortBy === "random") {
|
if (this.sortBy === "random") {
|
||||||
// #321 - set the random seed if it is not set
|
// #321 - set the random seed if it is not set
|
||||||
@@ -149,36 +186,55 @@ export class ListFilterModel {
|
|||||||
return this.sortBy;
|
return this.sortBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQueryParameters() {
|
// Returns query parameters with necessary parts encoded
|
||||||
const encodedCriteria: string[] = this.criteria.map((criterion) =>
|
public getQueryParameters(): IQueryParameters {
|
||||||
criterion.toJSON()
|
const encodedCriteria: string[] = this.criteria.map((criterion) => {
|
||||||
);
|
let str = criterion.toJSON();
|
||||||
|
// URL-encode other characters
|
||||||
|
str = encodeURI(str);
|
||||||
|
// force URL-encode existing ()
|
||||||
|
str = str.replaceAll("(", "%28");
|
||||||
|
str = str.replaceAll(")", "%29");
|
||||||
|
// replace JSON '{'(%7B) '}'(%7D) with explicitly unreserved ()
|
||||||
|
str = str.replaceAll("%7B", "(");
|
||||||
|
str = str.replaceAll("%7D", ")");
|
||||||
|
// only the reserved characters ?#&;=+ need to be URL-encoded
|
||||||
|
// as they have special meaning in query strings
|
||||||
|
str = str.replaceAll("?", encodeURIComponent("?"));
|
||||||
|
str = str.replaceAll("#", encodeURIComponent("#"));
|
||||||
|
str = str.replaceAll("&", encodeURIComponent("&"));
|
||||||
|
str = str.replaceAll(";", encodeURIComponent(";"));
|
||||||
|
str = str.replaceAll("=", encodeURIComponent("="));
|
||||||
|
str = str.replaceAll("+", encodeURIComponent("+"));
|
||||||
|
return str;
|
||||||
|
});
|
||||||
|
|
||||||
const result = {
|
return {
|
||||||
perPage:
|
perPage:
|
||||||
this.itemsPerPage !== DEFAULT_PARAMS.itemsPerPage
|
this.itemsPerPage !== DEFAULT_PARAMS.itemsPerPage
|
||||||
? this.itemsPerPage
|
? String(this.itemsPerPage)
|
||||||
: undefined,
|
: undefined,
|
||||||
sortby: this.getSortBy() ?? undefined,
|
sortby: this.getSortBy() ?? undefined,
|
||||||
sortdir:
|
sortdir:
|
||||||
this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined,
|
this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined,
|
||||||
disp:
|
disp:
|
||||||
this.displayMode !== DEFAULT_PARAMS.displayMode
|
this.displayMode !== DEFAULT_PARAMS.displayMode
|
||||||
? this.displayMode
|
? String(this.displayMode)
|
||||||
: undefined,
|
: undefined,
|
||||||
q: this.searchTerm ? encodeURIComponent(this.searchTerm) : undefined,
|
q: this.searchTerm ? encodeURIComponent(this.searchTerm) : undefined,
|
||||||
p:
|
p:
|
||||||
this.currentPage !== DEFAULT_PARAMS.currentPage
|
this.currentPage !== DEFAULT_PARAMS.currentPage
|
||||||
? this.currentPage
|
? String(this.currentPage)
|
||||||
|
: undefined,
|
||||||
|
z:
|
||||||
|
this.zoomIndex !== this.defaultZoomIndex
|
||||||
|
? String(this.zoomIndex)
|
||||||
: undefined,
|
: undefined,
|
||||||
z: this.zoomIndex !== this.defaultZoomIndex ? this.zoomIndex : undefined,
|
|
||||||
c: encodedCriteria,
|
c: encodedCriteria,
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSavedQueryParameters() {
|
public makeSavedFilterJSON() {
|
||||||
const encodedCriteria: string[] = this.criteria.map((criterion) =>
|
const encodedCriteria: string[] = this.criteria.map((criterion) =>
|
||||||
criterion.toJSON()
|
criterion.toJSON()
|
||||||
);
|
);
|
||||||
@@ -194,7 +250,7 @@ export class ListFilterModel {
|
|||||||
c: encodedCriteria,
|
c: encodedCriteria,
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return JSON.stringify(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public makeQueryParameters(): string {
|
public makeQueryParameters(): string {
|
||||||
|
|||||||
@@ -47,16 +47,6 @@ export function criterionIsNumberValue(
|
|||||||
return typeof value === "object" && "value" in value && "value2" in value;
|
return typeof value === "object" && "value" in value && "value2" in value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeLabel(v: string) {
|
|
||||||
// escape " and \ and by encoding to JSON so that it encodes to JSON correctly down the line
|
|
||||||
const adjustedLabel = JSON.stringify(v).slice(1, -1);
|
|
||||||
return encodeURIComponent(adjustedLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeILabeledId(o: ILabeledId) {
|
|
||||||
return { ...o, label: encodeLabel(o.label) };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IOptionType {
|
export interface IOptionType {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ import { SceneListFilterOptions } from "./list-filter/scenes";
|
|||||||
|
|
||||||
export type QueuedScene = Pick<Scene, "id" | "title" | "paths">;
|
export type QueuedScene = Pick<Scene, "id" | "title" | "paths">;
|
||||||
|
|
||||||
interface IQueryParameters {
|
|
||||||
qsort?: string;
|
|
||||||
qsortd?: string;
|
|
||||||
qfq?: string;
|
|
||||||
qfp?: string;
|
|
||||||
qfc?: string[];
|
|
||||||
qs?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPlaySceneOptions {
|
export interface IPlaySceneOptions {
|
||||||
sceneIndex?: number;
|
sceneIndex?: number;
|
||||||
newPage?: number;
|
newPage?: number;
|
||||||
@@ -88,7 +79,7 @@ export class SceneQueue {
|
|||||||
|
|
||||||
public static fromQueryParameters(params: string) {
|
public static fromQueryParameters(params: string) {
|
||||||
const ret = new SceneQueue();
|
const ret = new SceneQueue();
|
||||||
const parsed = queryString.parse(params) as IQueryParameters;
|
const parsed = queryString.parse(params, { decode: false });
|
||||||
const translated = {
|
const translated = {
|
||||||
sortby: parsed.qsort,
|
sortby: parsed.qsort,
|
||||||
sortdir: parsed.qsortd,
|
sortdir: parsed.qsortd,
|
||||||
@@ -98,11 +89,12 @@ export class SceneQueue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (parsed.qfp) {
|
if (parsed.qfp) {
|
||||||
|
const decoded = ListFilterModel.decodeQueryParameters(translated);
|
||||||
const query = new ListFilterModel(
|
const query = new ListFilterModel(
|
||||||
FilterMode.Scenes,
|
FilterMode.Scenes,
|
||||||
translated as queryString.ParsedQuery,
|
|
||||||
SceneListFilterOptions.defaultSortBy
|
SceneListFilterOptions.defaultSortBy
|
||||||
);
|
);
|
||||||
|
query.configureFromQueryParameters(decoded);
|
||||||
ret.query = query;
|
ret.query = query;
|
||||||
} else if (parsed.qs) {
|
} else if (parsed.qs) {
|
||||||
// must be scene list
|
// must be scene list
|
||||||
|
|||||||
Reference in New Issue
Block a user