feeds-fun 1.22.4 → 1.22.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codespell.toml +2 -0
- package/package.json +1 -1
- package/src/components/SimplePagination.vue +7 -7
- package/src/components/tags/EntryTagsList.vue +4 -4
- package/src/components/tags/TagsFilter.vue +4 -4
- package/src/layouts/SidePanelLayout.vue +1 -1
- package/src/logic/settings.ts +1 -1
- package/src/logic/tests/utils.test.ts +40 -0
- package/src/logic/utils.ts +24 -1
- package/src/stores/globalSettings.ts +8 -5
- package/vitest.config.ts +1 -0
package/codespell.toml
ADDED
package/package.json
CHANGED
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
{{ realShowEntries }} of {{ total }}
|
|
4
4
|
|
|
5
|
-
<button
|
|
6
|
-
class="ffun-form-button short ml-2"
|
|
7
|
-
v-if="canShowMore"
|
|
8
|
-
@click.prevent="showMore()">
|
|
9
|
-
next {{ realShowPerPage }}
|
|
10
|
-
</button>
|
|
11
|
-
|
|
12
5
|
<button
|
|
13
6
|
class="ffun-form-button short ml-2"
|
|
14
7
|
v-if="canHide"
|
|
@@ -16,6 +9,13 @@
|
|
|
16
9
|
>hide</button
|
|
17
10
|
>
|
|
18
11
|
|
|
12
|
+
<button
|
|
13
|
+
class="ffun-form-button short ml-2"
|
|
14
|
+
v-if="canShowMore"
|
|
15
|
+
@click.prevent="showMore()">
|
|
16
|
+
next {{ realShowPerPage }}
|
|
17
|
+
</button>
|
|
18
|
+
|
|
19
19
|
<div
|
|
20
20
|
v-if="counterOnNewLine"
|
|
21
21
|
style="line-height: 0.5rem"
|
|
@@ -85,14 +85,14 @@
|
|
|
85
85
|
return 1;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const
|
|
89
|
-
const
|
|
88
|
+
const leftTagCount = properties.tagsCount[a];
|
|
89
|
+
const rightTagCount = properties.tagsCount[b];
|
|
90
90
|
|
|
91
|
-
if (
|
|
91
|
+
if (leftTagCount > rightTagCount) {
|
|
92
92
|
return -1;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
if (
|
|
95
|
+
if (leftTagCount < rightTagCount) {
|
|
96
96
|
return 1;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -97,14 +97,14 @@
|
|
|
97
97
|
const counts = properties.tags;
|
|
98
98
|
|
|
99
99
|
function tagComparator(a: string, b: string) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
100
|
+
const leftTagCount = counts[a];
|
|
101
|
+
const rightTagCount = counts[b];
|
|
102
102
|
|
|
103
|
-
if (
|
|
103
|
+
if (leftTagCount > rightTagCount) {
|
|
104
104
|
return -1;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (
|
|
107
|
+
if (leftTagCount < rightTagCount) {
|
|
108
108
|
return 1;
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
|
|
166
166
|
if (globalState.logoutConfirmed) {
|
|
167
167
|
// Redirect to login page in case the user is not logged in.
|
|
168
|
-
// We redirect to login instead of the main page to be
|
|
168
|
+
// We redirect to login instead of the main page to be consistent
|
|
169
169
|
// with default API behavior on redirection in case of getting 401 status.
|
|
170
170
|
api.redirectToLogin();
|
|
171
171
|
}
|
package/src/logic/settings.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const version = __APP_VERSION__;
|
|
|
4
4
|
|
|
5
5
|
export const authRefreshInterval = import.meta.env.VITE_FFUN_AUTH_REFRESH_INTERVAL || 10 * 60 * 1000;
|
|
6
6
|
|
|
7
|
-
export const blog = import.meta.env.VITE_FFUN_BLOG || "https://
|
|
7
|
+
export const blog = import.meta.env.VITE_FFUN_BLOG || "https://feeds.fun/blog";
|
|
8
8
|
export const roadmap = import.meta.env.VITE_FFUN_ROADMAP || "https://github.com/users/Tiendil/projects/1";
|
|
9
9
|
export const githubRepo = import.meta.env.VITE_FFUN_GITHUB_REPO || "https://github.com/Tiendil/feeds.fun";
|
|
10
10
|
export const discordInvite = import.meta.env.VITE_FFUN_DISCORD_INVITE || "https://discord.gg/C5RVusHQXy";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it, vi} from "vitest";
|
|
2
|
+
import DOMPurify from "dompurify";
|
|
3
|
+
import {purifyBody, purifyTitle} from "@/logic/utils";
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("purifyBody", () => {
|
|
10
|
+
it("sets required security attributes for links", () => {
|
|
11
|
+
const raw = '<a href="https://example.com" target="_self" rel="noopener" referrerpolicy="unsafe-url">Example</a>';
|
|
12
|
+
|
|
13
|
+
const purified = purifyBody({raw, default_: "No description"});
|
|
14
|
+
|
|
15
|
+
expect(purified).toContain('href="https://example.com"');
|
|
16
|
+
expect(purified).toContain('target="_blank"');
|
|
17
|
+
expect(purified).toContain('rel="noopener noreferrer nofollow"');
|
|
18
|
+
expect(purified).toContain('referrerpolicy="strict-origin-when-cross-origin"');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("purifyTitle", () => {
|
|
23
|
+
it("returns default value for null and empty values", () => {
|
|
24
|
+
expect(purifyTitle({raw: null, default_: "No title"})).toBe("No title");
|
|
25
|
+
expect(purifyTitle({raw: " ", default_: "No title"})).toBe("No title");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("sets required security attributes for links in sanitized output", () => {
|
|
29
|
+
vi.spyOn(DOMPurify, "sanitize").mockReturnValue(
|
|
30
|
+
'<a href="https://example.com" target="_self" rel="noopener" referrerpolicy="unsafe-url">Example</a>'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const purified = purifyTitle({raw: "Example title", default_: "No title"});
|
|
34
|
+
|
|
35
|
+
expect(purified).toContain('href="https://example.com"');
|
|
36
|
+
expect(purified).toContain('target="_blank"');
|
|
37
|
+
expect(purified).toContain('rel="noopener noreferrer nofollow"');
|
|
38
|
+
expect(purified).toContain('referrerpolicy="strict-origin-when-cross-origin"');
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/logic/utils.ts
CHANGED
|
@@ -2,6 +2,25 @@ import _ from "lodash";
|
|
|
2
2
|
import type * as t from "@/logic/types";
|
|
3
3
|
import DOMPurify from "dompurify";
|
|
4
4
|
|
|
5
|
+
const REQUIRED_LINK_ATTRIBUTES = {
|
|
6
|
+
target: "_blank",
|
|
7
|
+
rel: "noopener noreferrer nofollow",
|
|
8
|
+
referrerpolicy: "strict-origin-when-cross-origin"
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
function hardenLinksSecurityAttributes(html: string) {
|
|
12
|
+
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
13
|
+
const links = parsed.body.querySelectorAll("[href]");
|
|
14
|
+
|
|
15
|
+
for (const link of links) {
|
|
16
|
+
link.setAttribute("target", REQUIRED_LINK_ATTRIBUTES.target);
|
|
17
|
+
link.setAttribute("rel", REQUIRED_LINK_ATTRIBUTES.rel);
|
|
18
|
+
link.setAttribute("referrerpolicy", REQUIRED_LINK_ATTRIBUTES.referrerpolicy);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return parsed.body.innerHTML;
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
export function timeSince(date: Date) {
|
|
6
25
|
const now = new Date();
|
|
7
26
|
|
|
@@ -80,6 +99,8 @@ export function purifyTitle({raw, default_}: {raw: string | null; default_: stri
|
|
|
80
99
|
return default_;
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
title = hardenLinksSecurityAttributes(title);
|
|
103
|
+
|
|
83
104
|
return title;
|
|
84
105
|
}
|
|
85
106
|
|
|
@@ -94,6 +115,8 @@ export function purifyBody({raw, default_}: {raw: string | null; default_: strin
|
|
|
94
115
|
return default_;
|
|
95
116
|
}
|
|
96
117
|
|
|
118
|
+
body = hardenLinksSecurityAttributes(body);
|
|
119
|
+
|
|
97
120
|
return body;
|
|
98
121
|
}
|
|
99
122
|
|
|
@@ -114,7 +137,7 @@ export function chooseTagByUsage({
|
|
|
114
137
|
exclude = [];
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
const tags = _.toPairs(tagsCount).sort((a, b) => {
|
|
140
|
+
const tags = _.toPairs(tagsCount).sort((a: [string, number], b: [string, number]) => {
|
|
118
141
|
if (a[1] === b[1]) {
|
|
119
142
|
return a[0].localeCompare(b[0]);
|
|
120
143
|
}
|
|
@@ -73,7 +73,7 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
|
73
73
|
// This dict is used for two purposes:
|
|
74
74
|
// - To store settings that anonymous user changes while using the site.
|
|
75
75
|
// - To close fast reactive loop after calling backendSettings.set.
|
|
76
|
-
// Without this, setting a setting will cause
|
|
76
|
+
// Without this, setting a setting will cause weird and complex chain
|
|
77
77
|
// of (re)loading data from the backend.
|
|
78
78
|
var settingsOverrides = ref<{[key in keyof any]: t.UserSettingsValue}>({});
|
|
79
79
|
|
|
@@ -139,19 +139,22 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
function enumBackendSettings
|
|
143
|
-
|
|
142
|
+
function enumBackendSettings<TValue extends t.UserSettingsValue, TProperty extends {default?: boolean}>(
|
|
143
|
+
kind: string,
|
|
144
|
+
enumProperties: Map<TValue, TProperty>
|
|
145
|
+
) {
|
|
146
|
+
const defaultEntry = _.find([...enumProperties], ([, prop]: [TValue, TProperty]) => prop.default === true);
|
|
144
147
|
|
|
145
148
|
if (!defaultEntry) {
|
|
146
149
|
throw new Error(`No default entry found for enum "${kind}"`);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
let defaultValue = defaultEntry[0];
|
|
152
|
+
let defaultValue: TValue = defaultEntry[0];
|
|
150
153
|
|
|
151
154
|
return backendSettings(
|
|
152
155
|
kind,
|
|
153
156
|
(rawValue: t.UserSettingsValue) => {
|
|
154
|
-
return enumProperties.has(rawValue);
|
|
157
|
+
return enumProperties.has(rawValue as TValue);
|
|
155
158
|
},
|
|
156
159
|
defaultValue
|
|
157
160
|
);
|