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 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.3",
3
+ "version": "1.22.5",
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
  }
@@ -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
 
@@ -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