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 ADDED
@@ -0,0 +1,2 @@
1
+ [tool.codespell]
2
+ ignore-words-list = "alltime,ontext"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feeds-fun",
3
- "version": "1.22.4",
3
+ "version": "1.22.6",
4
4
  "author": "Aliaksei Yaletski (Tiendil) <a.eletsky@gmail.com> (https://tiendil.org/)",
5
5
  "description": "Frontend for the Feeds Fun — web-based news reader",
6
6
  "keywords": [
@@ -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 aCount = properties.tagsCount[a];
89
- const bCount = properties.tagsCount[b];
88
+ const leftTagCount = properties.tagsCount[a];
89
+ const rightTagCount = properties.tagsCount[b];
90
90
 
91
- if (aCount > bCount) {
91
+ if (leftTagCount > rightTagCount) {
92
92
  return -1;
93
93
  }
94
94
 
95
- if (aCount < bCount) {
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 aCount = counts[a];
101
- const bCount = counts[b];
100
+ const leftTagCount = counts[a];
101
+ const rightTagCount = counts[b];
102
102
 
103
- if (aCount > bCount) {
103
+ if (leftTagCount > rightTagCount) {
104
104
  return -1;
105
105
  }
106
106
 
107
- if (aCount < bCount) {
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 consisten
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
  }
@@ -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://blog.feeds.fun";
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
+ });
@@ -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 weired and complex chain
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(kind: string, enumProperties: any) {
143
- const defaultEntry = _.find([...enumProperties], ([, prop]) => prop.default);
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
  );
package/vitest.config.ts CHANGED
@@ -8,6 +8,7 @@ export default mergeConfig(
8
8
  defineConfig({
9
9
  test: {
10
10
  environment: 'jsdom',
11
+ dir: 'src',
11
12
  exclude: [...configDefaults.exclude, 'e2e/*'],
12
13
  root: fileURLToPath(new URL('./', import.meta.url))
13
14
  }