feeds-fun 1.22.3 → 1.22.5
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/tests/utils.test.ts +40 -0
- package/src/logic/utils.ts +23 -0
- package/src/stores/globalSettings.ts +1 -1
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
|
}
|
|
@@ -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
|
|
|
@@ -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
|
|