Skip to content

Commit d6db32e

Browse files
feat: Allow to test common feeds URLs patterns
1 parent edacb6d commit d6db32e

File tree

4 files changed

+145
-17
lines changed

4 files changed

+145
-17
lines changed

src/i18n.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export const i18n = createI18n({
2929
"An unknown error has occurred. Please close and reopen the extension. If it still doesn’t work, please contact the support.",
3030
"errors.url.presence": "Enter a URL.",
3131
"errors.url.url": "The URL of the current page is not supported by Flus.",
32+
"feeds.autotest.invalid_url":
33+
"Unable to test URLs automatically as the tab URL looks invalid.",
34+
"feeds.autotest.missing_permissions":
35+
"You must authorize the extension to access all websites in the browser settings.",
36+
"feeds.autotest.no_results": "No URL corresponding to a feed was found.",
37+
"feeds.autotest.submit": "Test URLs automatically",
3238
"feeds.count_detected":
3339
"No feeds detected on this page. | 1 feed detected on this page. | {count} feeds detected on this page.",
3440
"feeds.follow": "Follow",
@@ -41,6 +47,7 @@ export const i18n = createI18n({
4147
"feeds.unfollow": "Unfollow",
4248
"feeds.url_potential_feed": "URL of a potential feed",
4349
"forms.error": "Error:",
50+
"forms.or": "or",
4451
"link.count_collections": "No collections | 1 collection | {count} collections",
4552
"link.invalid_protocol":
4653
"This page cannot be handled by Flus (non-supported protocol).",
@@ -56,7 +63,6 @@ export const i18n = createI18n({
5663
"login.errors.server_error":
5764
"The server “{server}” cannot be reached, please check its address.",
5865
"login.intro": "Log in to access Flus.",
59-
"login.or": "or",
6066
"login.password.label": "Password",
6167
"login.register": "create an account",
6268
"login.server": "Server:",
@@ -112,6 +118,12 @@ export const i18n = createI18n({
112118
"Une erreur inconnue est survenue. Veuillez fermer et réouvrir l’extension. Si cela ne suffit pas, veuillez contacter le support.",
113119
"errors.url.presence": "Saisissez une URL.",
114120
"errors.url.url": "L’URL de la page actuelle n’est pas supportée par Flus.",
121+
"feeds.autotest.invalid_url":
122+
"Impossible de tester les URL automatiquement car l’URL de l’onglet semble invalide.",
123+
"feeds.autotest.missing_permissions":
124+
"Vous devez autoriser l’extension à accéder à tous les sites dans les paramètres du navigateur.",
125+
"feeds.autotest.no_results": "Aucune URL correspondante à un flux n’a été trouvée.",
126+
"feeds.autotest.submit": "Tester des URL automatiquement",
115127
"feeds.count_detected":
116128
"Aucun flux détecté sur cette page. | 1 flux détecté sur cette page. | {count} flux détectés sur cette page.",
117129
"feeds.follow": "Suivre",
@@ -124,6 +136,7 @@ export const i18n = createI18n({
124136
"feeds.unfollow": "Ne plus suivre",
125137
"feeds.url_potential_feed": "URL d’un flux potentiel",
126138
"forms.error": "Erreur :",
139+
"forms.or": "ou",
127140
"link.count_collections": "Aucune collection | 1 collection | {count} collections",
128141
"link.invalid_protocol":
129142
"Cette page ne peut pas être enregistrée dans Flus (protocole non supporté).",
@@ -139,7 +152,6 @@ export const i18n = createI18n({
139152
"login.errors.server_error":
140153
"Le serveur « {server} » est injoignable, veuillez vérifier son adresse.",
141154
"login.intro": "Identifiez-vous pour accéder à votre veille Flus.",
142-
"login.or": "ou",
143155
"login.password.label": "Mot de passe",
144156
"login.register": "créer un compte",
145157
"login.server": "Serveur :",

src/manifest.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
"permissions": ["tabs", "storage"],
2020

21+
"host_permissions": ["http://*/*", "https://*/*"],
22+
2123
"action": {
2224
"default_icon": "icons/icon-32.png",
2325
"default_title": "Flus",

src/screens/FeedsScreen.vue

Lines changed: 128 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
</ul>
4747

4848
<div v-else class="flow">
49+
<hr>
50+
4951
<p>
5052
{{ t("feeds.search_tip") }}
5153
</p>
@@ -65,22 +67,37 @@
6567
{{ form.error('url') }}
6668
</p>
6769

68-
<input
69-
v-model="urlToTest"
70-
type="url"
71-
id="url"
72-
placeholder="https://…"
73-
required
74-
:aria-invalid="form.isInvalid('url')"
75-
:aria-errormessage="form.isInvalid('url') ? 'url-error' : null"
76-
:disabled="form.inProgress()"
77-
/>
70+
<div class="cols cols--always cols--gap-smaller">
71+
<input
72+
v-model="urlToTest"
73+
type="url"
74+
id="url"
75+
placeholder="https://…"
76+
required
77+
:aria-invalid="form.isInvalid('url')"
78+
:aria-errormessage="form.isInvalid('url') ? 'url-error' : null"
79+
:disabled="form.inProgress()"
80+
/>
81+
82+
<button class="button--primary button--big" :disabled="form.inProgress()">
83+
{{ t('feeds.test') }}
84+
</button>
85+
</div>
7886
</div>
7987

80-
<div class="text--center">
81-
<button class="button--primary button--big" :disabled="form.inProgress()">
82-
{{ t('feeds.test') }}
83-
</button>
88+
<div aria-live="polite">
89+
<div v-if="!autotestResult">
90+
{{ t('forms.or') }}
91+
92+
<button type="button" @click="testCommonFeedsPatterns">
93+
{{ t('feeds.autotest.submit') }}
94+
</button>
95+
</div>
96+
97+
<p v-else class="form-group__error">
98+
<Icon name="error"></Icon>
99+
{{ autotestResult }}
100+
</p>
84101
</div>
85102
</form>
86103
</div>
@@ -138,6 +155,7 @@ const alert = ref({
138155
const feeds = ref([]);
139156
140157
const urlToTest = ref("");
158+
const autotestResult = ref("");
141159
142160
function feedUrl(feed) {
143161
return `${store.auth.server}/collections/${feed.id}`;
@@ -155,6 +173,8 @@ async function getCurrentTab() {
155173
}
156174
157175
function refreshForUrl(url) {
176+
form.startRequest();
177+
158178
api.search(url)
159179
.then((data) => {
160180
// This array is used to deduplicate feeds with the same name.
@@ -171,6 +191,7 @@ function refreshForUrl(url) {
171191
}
172192
});
173193
ready.value = true;
194+
form.finishRequest();
174195
})
175196
.catch((error) => {
176197
if (error instanceof http.HttpError) {
@@ -183,6 +204,7 @@ function refreshForUrl(url) {
183204
}
184205
185206
ready.value = true;
207+
form.finishRequest();
186208
});
187209
}
188210
@@ -233,5 +255,97 @@ function testUrl() {
233255
refreshForUrl(urlToTest.value);
234256
}
235257
258+
async function testCommonFeedsPatterns() {
259+
// Try not to add too many patterns as they generate several HTTP requests.
260+
const commonFeedsPatterns = [
261+
"/feed",
262+
"/feed.xml",
263+
"/rss.xml",
264+
"/atom.xml",
265+
"/index.xml",
266+
"/rss",
267+
"/rss/",
268+
"/rss/feed.xml",
269+
];
270+
271+
const tabUrl = (await getCurrentTab()).url;
272+
const parsedUrl = URL.parse(tabUrl);
273+
274+
if (!parsedUrl) {
275+
autotestResult.value = t("feeds.autotest.invalid_url");
276+
return;
277+
}
278+
279+
if (!(await hasPermissionsForAutotest())) {
280+
const result = await requestPermissionsForAutotest();
281+
if (!result) {
282+
autotestResult.value = t("feeds.autotest.missing_permissions");
283+
return;
284+
}
285+
}
286+
287+
form.startRequest();
288+
289+
const baseUrl = parsedUrl.origin;
290+
let foundFeed = false;
291+
292+
for (const urlPattern of commonFeedsPatterns) {
293+
const testedFeedUrl = baseUrl + urlPattern;
294+
295+
urlToTest.value = testedFeedUrl;
296+
297+
const response = await fetch(testedFeedUrl);
298+
299+
if (!response.ok) {
300+
continue;
301+
}
302+
303+
const content = await response.text();
304+
foundFeed = looksLikeFeedContent(content);
305+
306+
if (foundFeed) {
307+
break;
308+
}
309+
}
310+
311+
form.finishRequest();
312+
313+
if (foundFeed) {
314+
testUrl();
315+
} else {
316+
urlToTest.value = "";
317+
autotestResult.value = t("feeds.autotest.no_results");
318+
}
319+
}
320+
321+
function looksLikeFeedContent(content) {
322+
const parser = new DOMParser();
323+
const doc = parser.parseFromString(content, "application/xml");
324+
325+
const rootElement = doc.documentElement ? doc.documentElement.tagName : null;
326+
327+
return rootElement === "rss" || rootElement === "feed" || rootElement === "rdf";
328+
}
329+
330+
async function hasPermissionsForAutotest() {
331+
const origins = ["http://*/*", "https://*/*"];
332+
333+
return await browser.permissions.contains({ origins });
334+
}
335+
336+
async function requestPermissionsForAutotest() {
337+
const origins = ["http://*/*", "https://*/*"];
338+
339+
try {
340+
return await browser.permissions.request({ origins });
341+
} catch {
342+
// On Firefox, requesting the permission fails with the error:
343+
// "permissions.request may only be called from a user input handler".
344+
// See https://stackoverflow.com/q/47723297 for a bit more context. I
345+
// wasn't able to make it works though.
346+
return false;
347+
}
348+
}
349+
236350
onMounted(refreshForCurrentTab);
237351
</script>

src/screens/LoginScreen.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
</div>
107107

108108
<p>
109-
{{ t("login.or") }}
109+
{{ t("forms.or") }}
110110

111111
<a @click.prevent="openRegistrationPage" :href="registrationUrl">
112112
{{ t("login.register") }}

0 commit comments

Comments
 (0)