From 0a844aa1747193036961990e693804979f439019 Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Mon, 4 Nov 2019 15:32:55 +0100 Subject: [PATCH] Open links from event URL and in event description in external window And add rel='noopener noreferrer' on them Closes #282 and #283 Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- CHANGELOG.md | 9 ++-- config/config.exs | 4 +- js/src/i18n/en_US.json | 3 +- js/src/i18n/fr_FR.json | 41 ++++++++++--------- js/src/views/Event/Event.vue | 7 +++- .../service/formatter/formatter_test.exs | 25 ++++++----- 6 files changed, 49 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1316356f..39a7c56a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,13 +31,14 @@ In order to move participant stats to the event table for existing events, you n - Upgraded frontend and backend dependencies ### Changed -- Improve Docker setup and docs -- Handle error message difference between user not found and user not confirmed -- Upgrade vue-cli to v4, change the way server params injection is made +- Move participant stats to event table **(read special instructions above)** - Limit length (20 characters) and number (10) of tags allowed - Added some backend changes and validation for field length +- Handle error message difference between user not found and user not confirmed +- Make external links (from URL field and description) open in a new tab with `noopener` +- Improve Docker setup and docs +- Upgrade vue-cli to v4, change the way server params injection is made - Improve some production ipv6 configuration -- Move participant stats to event table **(read special instructions above)** ### Fixed - Fix event URL validation and check if hostname is correct before showing it diff --git a/config/config.exs b/config/config.exs index f117bea40..c4ad01e5d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -106,9 +106,7 @@ config :auto_linker, # TODO: Set to :no_scheme when it works properly validate_tld: true, class: false, - strip_prefix: false, - new_window: false, - rel: false + strip_prefix: false ] config :phoenix, :format_encoders, json: Jason, "activity-json": Jason diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index e1fa94eb7..a9e7dc339 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -278,6 +278,7 @@ "Users": "Users", "View event page": "View event page", "View everything": "View everything", + "View page on {hostname} (in a new window)": "View page on {hostname} (in a new window)", "Visible everywhere on the web (public)": "Visible everywhere on the web (public)", "Waiting for organization team approval.": "Waiting for organization team approval.", "Waiting list": "Waiting list", @@ -325,4 +326,4 @@ "{count} requests waiting": "{count} requests waiting", "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.", "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks" -} \ No newline at end of file +} diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 7ae20c601..d9666fa5b 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -3,14 +3,14 @@ "A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.", "A validation email was sent to {email}": "Un email de validation a été envoyé à {email}", "Abandon edition": "Abandonner l'édition", - "About": "À propos", "About Mobilizon": "À propos de Mobilizon", "About this event": "À propos de cet événement", "About this instance": "À propos de cette instance", - "Add": "Ajouter", + "About": "À propos", "Add an address": "Ajouter une adresse", "Add some tags": "Ajouter des tags", "Add to my calendar": "Ajouter à mon agenda", + "Add": "Ajouter", "Additional comments": "Commentaires additionnels", "Administration": "Administration", "All data will be deleted every 48 hours, so please don't use this for anything real.": "Toutes les données seront effacées toutes les 48 heures, donc n'utilisez pas ce site à des fins autres que de démonstration.", @@ -25,28 +25,27 @@ "Avatar": "Avatar", "Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte", "By {name}": "Par {name}", - "Cancel": "Annuler", "Cancel creation": "Annuler la création", "Cancel edition": "Annuler l'édition", "Cancel my participation request…": "Annuler ma demande de participation…", "Cancel my participation…": "Annuler ma participation…", + "Cancel": "Annuler", "Cancelled: Won't happen": "Annulé: N'aura pas lieu", "Category": "Catégorie", - "Change": "Modifier", "Change my identity…": "Changer mon identité…", "Change my password": "Modifier mon mot de passe", "Change password": "Modifier mot de passe", + "Change": "Modifier", "Clear": "Effacer", "Click to select": "Cliquez pour sélectionner", "Click to upload": "Cliquez pour uploader", "Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)", - "Comments": "Commentaires", "Comments on the event page": "Commentaires sur la page de l'événement", + "Comments": "Commentaires", "Confirm my particpation": "Confirmer ma participation", "Confirmed: Will happen": "Confirmé : aura lieu", "Continue editing": "Continuer l'édition", "Country": "Pays", - "Create": "Créer", "Create a new event": "Créer un nouvel événement", "Create a new group": "Créer un nouveau groupe", "Create a new identity": "Créer une nouvelle identité", @@ -57,16 +56,17 @@ "Create my profile": "Créer mon profil", "Create token": "Créer un jeton", "Create, edit or delete events": "Créer, modifier ou supprimer des événements", + "Create": "Créer", "Creator": "Créateur", "Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet événement.", "Date and time settings": "Paramètres de date et d'heure", "Date parameters": "Paramètres de date", - "Delete": "Supprimer", "Delete event": "Supprimer un événement", "Delete this identity": "Supprimer cette identité", "Delete your identity": "Supprimer votre identité", "Delete {eventTitle}": "Supprimer {eventTitle}", "Delete {preferredUsername}": "Supprimer {preferredUsername}", + "Delete": "Supprimer", "Description": "Description", "Didn't receive the instructions ?": "Vous n'avez pas reçu les instructions ?", "Display name": "Nom affiché", @@ -84,7 +84,6 @@ "Error while communicating with the server.": "Erreur de communication avec le serveur.", "Error while saving report.": "Erreur lors de l'enregistrement du signalement.", "Error while validating account": "Erreur lors de la validation du compte", - "Event": "Événement", "Event already passed": "Événement déjà passé", "Event cancelled": "Événement annulé", "Event creation": "Création d'événement", @@ -95,6 +94,7 @@ "Event to be confirmed": "Événement à confirmer", "Event {eventTitle} deleted": "Événement {eventTitle} supprimé", "Event {eventTitle} reported": "Événement {eventTitle} signalé", + "Event": "Événement", "Events": "Événements", "Exclude": "Exclure", "Explore": "Explorer", @@ -105,8 +105,8 @@ "For instance: London, Taekwondo, Architecture…": "Par exemple: Lyon, Taekwondo, Architecture…", "Forgot your password ?": "Mot de passe oublié ?", "From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants’ platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "De l’anniversaire entre ami·e·s à une marche pour le climat, aujourd’hui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment s’organiser, comment cliquer sur « je participe » sans <b>livrer des données intimes</b> à Facebook ou<b> s’enfermer</b> dans MeetUp ?", - "From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}", "From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}", + "From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}", "From the {startDate} to the {endDate}": "Du {startDate} au {endDate}", "Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser", "General information": "Informations générales", @@ -131,8 +131,8 @@ "Join {instance}, a Mobilizon instance": "Rejoignez {instance}, une instance Mobilizon", "Last published event": "Dernier événement publié", "Last week": "La semaine dernière", - "Learn more": "En apprendre plus", "Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon", + "Learn more": "En apprendre plus", "Leave event": "Annuler ma participation à l'événement", "Leaving event \"{title}\"": "Annuler ma participation à l'événement", "Let's create a new common": "Créons un nouveau Common", @@ -142,8 +142,8 @@ "Locality": "Commune", "Log in": "Se connecter", "Log out": "Se déconnecter", - "Login": "Se connecter", "Login on Mobilizon!": "Se connecter sur Mobilizon !", + "Login": "Se connecter", "Manage participations": "Gérer les participations", "Members": "Membres", "Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.": "Mobilizon est un logiciel libre qui permettra à des communautés de <b>créer leurs propres espaces</b> de publication d’événements, afin de mieux s’émanciper des géants du web.", @@ -165,15 +165,15 @@ "Number of places": "Nombre de places", "OK": "OK", "Old password": "Ancien mot de passe", - "On {date}": "Le {date}", "On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}", "On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}", "On {date} starting at {startTime}": "Le {date} à partir de {startTime}", + "On {date}": "Le {date}", "One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont", "Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)", "Opened reports": "Signalements ouverts", - "Organized": "Organisés", "Organized by {name}": "Organisé par {name}", + "Organized": "Organisés", "Organizer": "Organisateur", "Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.", "Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)", @@ -184,10 +184,10 @@ "Participate": "Participer", "Participation approval": "Validation des participations", "Participation requested!": "Participation demandée !", - "Password": "Mot de passe", "Password (confirmation)": "Mot de passe (confirmation)", "Password change": "Changement de mot de passe", "Password reset": "Réinitialisation du mot de passe", + "Password": "Mot de passe", "Past events": "Événements passés", "Pick an identity": "Choisissez une identité", "Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.", @@ -209,23 +209,23 @@ "RSS/Atom Feed": "Flux RSS/Atom", "Read Framasoft’s statement of intent on the Framablog": "Lire la note d’intention de Framasoft sur le Framablog", "Region": "Région", - "Register": "S'inscrire", "Register an account on Mobilizon!": "S'inscrire sur Mobilizon !", "Register for an event by choosing one of your identities": "S'inscrire à un événement en choisissant une de vos identités", + "Register": "S'inscrire", "Registration is currently closed.": "Les inscriptions sont actuellement fermées.", "Reject": "Rejetter", - "Rejected": "Rejetés", "Rejected participations": "Participations rejetées", - "Report": "Signaler", + "Rejected": "Rejetés", "Report this event": "Signaler cet événement", + "Report": "Signaler", "Requests": "Requêtes", "Resend confirmation email": "Envoyer à nouveau l'email de confirmation", "Reset my password": "Réinitialiser mon mot de passe", - "Save": "Enregistrer", "Save draft": "Enregistrer le brouillon", - "Search": "Rechercher", + "Save": "Enregistrer", "Search events, groups, etc.": "Rechercher des événements, des groupes, etc.", "Search results: \"{search}\"": "Résultats de recherche: « {search} »", + "Search": "Rechercher", "Searching…": "Recherche en cours…", "Send me an email to reset my password": "Envoyez-moi un email pour réinitialiser mon mot de passe", "Send me the confirmation email once again": "Envoyez-moi l'email de confirmation encore une fois", @@ -246,8 +246,8 @@ "The draft event has been updated": "L'événement brouillon a été mis à jour", "The event has been created as a draft": "L'événement a été créé en tant que brouillon", "The event has been published": "L'événement a été publié", - "The event has been updated": "L'événement a été mis à jour", "The event has been updated and published": "L'événement a été mis à jour et publié", + "The event has been updated": "L'événement a été mis à jour", "The event organizer didn't add any description.": "L'organisateur de l'événement n'a pas ajouté de description.", "The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.", "The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.", @@ -278,6 +278,7 @@ "Users": "Utilisateurs", "View event page": "Voir la page de l'événement", "View everything": "Voir tout", + "View page on {hostname} (in a new window)": "Voir la page sur {hostname} (dans une nouvelle fenêtre)", "Visible everywhere on the web (public)": "Visible partout sur le web (public)", "Waiting for organization team approval.": "En attente d'approbation par l'organisation.", "Waiting list": "Liste d'attente", diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index 7819e9d48..62e66c9c5 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -134,7 +134,12 @@ import {ParticipantRole} from "@/types/event.model"; </div> <span class="online-address" v-if="event.onlineAddress && urlToHostname(event.onlineAddress)"> <b-icon icon="link"></b-icon> - <a :href="event.onlineAddress">{{ urlToHostname(event.onlineAddress) }}</a> + <a + target="_blank" + rel="noopener noreferrer" + :href="event.onlineAddress" + :title="$t('View page on {hostname} (in a new window)', {hostname: urlToHostname(event.onlineAddress) })" + >{{ urlToHostname(event.onlineAddress) }}</a> </span> <div class="organizer"> <span> diff --git a/test/mobilizon/service/formatter/formatter_test.exs b/test/mobilizon/service/formatter/formatter_test.exs index cc3dcfa40..47588c440 100644 --- a/test/mobilizon/service/formatter/formatter_test.exs +++ b/test/mobilizon/service/formatter/formatter_test.exs @@ -33,21 +33,21 @@ defmodule Mobilizon.Service.FormatterTest do text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." expected = - "Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ." + "Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ." assert {^expected, [], []} = Formatter.linkify(text) text = "https://mastodon.social/@lambadalambda" expected = - "<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>" + "<a href=\"https://mastodon.social/@lambadalambda\" target=\"_blank\" rel=\"noopener noreferrer\">https://mastodon.social/@lambadalambda</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "https://mastodon.social:4000/@lambadalambda" expected = - "<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>" + "<a href=\"https://mastodon.social:4000/@lambadalambda\" target=\"_blank\" rel=\"noopener noreferrer\">https://mastodon.social:4000/@lambadalambda</a>" assert {^expected, [], []} = Formatter.linkify(text) @@ -57,55 +57,58 @@ defmodule Mobilizon.Service.FormatterTest do assert {^expected, [], []} = Formatter.linkify(text) text = "http://www.cs.vu.nl/~ast/intel/" - expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>" + + expected = + "<a href=\"http://www.cs.vu.nl/~ast/intel/\" target=\"_blank\" rel=\"noopener noreferrer\">http://www.cs.vu.nl/~ast/intel/</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" expected = - "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>" + "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\" target=\"_blank\" rel=\"noopener noreferrer\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" expected = - "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>" + "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\" target=\"_blank\" rel=\"noopener noreferrer\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "https://www.google.co.jp/search?q=Nasim+Aghdam" expected = - "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>" + "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\" target=\"_blank\" rel=\"noopener noreferrer\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "https://en.wikipedia.org/wiki/Duff's_device" expected = - "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>" + "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\" target=\"_blank\" rel=\"noopener noreferrer\">https://en.wikipedia.org/wiki/Duff's_device</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "https://pleroma.com https://pleroma.com/sucks" expected = - "<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>" + "<a href=\"https://pleroma.com\" target=\"_blank\" rel=\"noopener noreferrer\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\" target=\"_blank\" rel=\"noopener noreferrer\">https://pleroma.com/sucks</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "xmpp:contact@hacktivis.me" - expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>" + expected = + "<a href=\"xmpp:contact@hacktivis.me\" target=\"_blank\" rel=\"noopener noreferrer\">xmpp:contact@hacktivis.me</a>" assert {^expected, [], []} = Formatter.linkify(text) text = "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" - expected = "<a href=\"#{text}\">#{text}</a>" + expected = "<a href=\"#{text}\" target=\"_blank\" rel=\"noopener noreferrer\">#{text}</a>" assert {^expected, [], []} = Formatter.linkify(text) end