From d1c57bad7cbb69f36c71cbe99c4b4d28f282f4fa Mon Sep 17 00:00:00 2001 From: Mehdi El Oualy Date: Mon, 16 Dec 2024 17:40:53 +0100 Subject: [PATCH 1/5] Allow right click to inspect project This allows right click in /accounts/profile/#notifications in the watched project list. So users can open a project and inspect it before follwing it or not, as was suggested in issue #12434 --- weblate/accounts/forms.py | 3 +++ weblate/static/js/accounts/profile/index.js | 20 ++++++++++++++++++++ weblate/templates/accounts/profile.html | 10 ++++++++++ 3 files changed, 33 insertions(+) create mode 100644 weblate/static/js/accounts/profile/index.js diff --git a/weblate/accounts/forms.py b/weblate/accounts/forms.py index c2c816a94fcf..e99df451eee9 100644 --- a/weblate/accounts/forms.py +++ b/weblate/accounts/forms.py @@ -274,6 +274,9 @@ def __init__(self, *args, **kwargs) -> None: user = kwargs["instance"].user self.fields["watched"].required = False self.fields["watched"].queryset = user.allowed_projects + self.fields["watched"].choices = [ + (x.slug, x.name) for x in user.allowed_projects + ] self.helper = FormHelper(self) self.helper.disable_csrf = True self.helper.form_tag = False diff --git a/weblate/static/js/accounts/profile/index.js b/weblate/static/js/accounts/profile/index.js new file mode 100644 index 000000000000..53678aa4a758 --- /dev/null +++ b/weblate/static/js/accounts/profile/index.js @@ -0,0 +1,20 @@ +// Copyright © Michal Čihař +// +// SPDX-License-Identifier: GPL-3.0-or-later + +$(document).ready(() => { + const $profileNotificationSettings = $("#notifications"); + + // Open project page on right click + $profileNotificationSettings.on("contextmenu", (e) => { + if ($(e.target).closest(".multi-wrapper a").length > 0) { + const slug = $(e.target).closest(".multi-wrapper a").data("value"); + if (slug) { + window.location.href = `/projects/${slug}`; + } + // Prevent context menu from displaying + e.preventDefault(); + return true; + } + }); +}); diff --git a/weblate/templates/accounts/profile.html b/weblate/templates/accounts/profile.html index c50bf9453b82..332b5541394b 100644 --- a/weblate/templates/accounts/profile.html +++ b/weblate/templates/accounts/profile.html @@ -1,12 +1,22 @@ {% extends "base.html" %} +{% load compress %} {% load i18n %} +{% load static %} {% load translations %} {% load authnames %} {% load crispy_forms_tags %} {% load icons %} {% load otp_webauthn %} +{% block extra_script %} + {% compress js %} + + {% endcompress %} +{% endblock %} + {% block breadcrumbs %}
  • {% trans "Your profile" %} From 2e88565a0487d3e5d1bbc78aa2daa5c0de4b20b1 Mon Sep 17 00:00:00 2001 From: Mehdi El Oualy Date: Tue, 17 Dec 2024 14:20:55 +0100 Subject: [PATCH 2/5] Open project in new tab and add info label --- weblate/accounts/models.py | 3 ++- weblate/static/js/accounts/profile/index.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/weblate/accounts/models.py b/weblate/accounts/models.py index 48b2d00f3c07..bc69cf6dc0e1 100644 --- a/weblate/accounts/models.py +++ b/weblate/accounts/models.py @@ -632,7 +632,8 @@ class Profile(models.Model): verbose_name=gettext_lazy("Watched projects"), help_text=gettext_lazy( "You can receive notifications for watched projects and " - "they are shown on the dashboard by default." + "they are shown on the dashboard by default. " + "You can right click on a project above to inspect it, before choosing." ), blank=True, ) diff --git a/weblate/static/js/accounts/profile/index.js b/weblate/static/js/accounts/profile/index.js index 53678aa4a758..5e354409b06a 100644 --- a/weblate/static/js/accounts/profile/index.js +++ b/weblate/static/js/accounts/profile/index.js @@ -10,7 +10,7 @@ $(document).ready(() => { if ($(e.target).closest(".multi-wrapper a").length > 0) { const slug = $(e.target).closest(".multi-wrapper a").data("value"); if (slug) { - window.location.href = `/projects/${slug}`; + window.open(`/projects/${slug}`, "_blank"); } // Prevent context menu from displaying e.preventDefault(); From 1212ef2aa9a0444715cbbb6474fd8b305e936743 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Dec 2024 14:42:59 +0100 Subject: [PATCH 3/5] Add missing migeration --- .../migrations/0015_alter_profile_watched.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 weblate/accounts/migrations/0015_alter_profile_watched.py diff --git a/weblate/accounts/migrations/0015_alter_profile_watched.py b/weblate/accounts/migrations/0015_alter_profile_watched.py new file mode 100644 index 000000000000..8e55e3f8d2a9 --- /dev/null +++ b/weblate/accounts/migrations/0015_alter_profile_watched.py @@ -0,0 +1,27 @@ +# Copyright © Michal Čihař +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Generated by Django 5.1.4 on 2024-12-17 13:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0014_alter_subscription_unique_together_and_more"), + ("trans", "0025_alter_announcement_notify"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="watched", + field=models.ManyToManyField( + blank=True, + help_text="You can receive notifications for watched projects and they are shown on the dashboard by default. You can right click on a project above to inspect it, before choosing.", + to="trans.project", + verbose_name="Watched projects", + ), + ), + ] From 4d7f2254c836c7e24ad5b79af95263302870a691 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 20 Dec 2024 13:51:39 +0100 Subject: [PATCH 4/5] Make languages elements link link They behave exactly like a normal link (right/mid-clickable), but noraml click is used to select and unselect. --- .../migrations/0015_alter_profile_watched.py | 27 ------------- weblate/accounts/models.py | 3 +- weblate/static/js/accounts/profile/index.js | 40 ++++++++++++++----- 3 files changed, 30 insertions(+), 40 deletions(-) delete mode 100644 weblate/accounts/migrations/0015_alter_profile_watched.py diff --git a/weblate/accounts/migrations/0015_alter_profile_watched.py b/weblate/accounts/migrations/0015_alter_profile_watched.py deleted file mode 100644 index 8e55e3f8d2a9..000000000000 --- a/weblate/accounts/migrations/0015_alter_profile_watched.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright © Michal Čihař -# -# SPDX-License-Identifier: GPL-3.0-or-later - -# Generated by Django 5.1.4 on 2024-12-17 13:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("accounts", "0014_alter_subscription_unique_together_and_more"), - ("trans", "0025_alter_announcement_notify"), - ] - - operations = [ - migrations.AlterField( - model_name="profile", - name="watched", - field=models.ManyToManyField( - blank=True, - help_text="You can receive notifications for watched projects and they are shown on the dashboard by default. You can right click on a project above to inspect it, before choosing.", - to="trans.project", - verbose_name="Watched projects", - ), - ), - ] diff --git a/weblate/accounts/models.py b/weblate/accounts/models.py index bc69cf6dc0e1..48b2d00f3c07 100644 --- a/weblate/accounts/models.py +++ b/weblate/accounts/models.py @@ -632,8 +632,7 @@ class Profile(models.Model): verbose_name=gettext_lazy("Watched projects"), help_text=gettext_lazy( "You can receive notifications for watched projects and " - "they are shown on the dashboard by default. " - "You can right click on a project above to inspect it, before choosing." + "they are shown on the dashboard by default." ), blank=True, ) diff --git a/weblate/static/js/accounts/profile/index.js b/weblate/static/js/accounts/profile/index.js index 5e354409b06a..d82807e42cd1 100644 --- a/weblate/static/js/accounts/profile/index.js +++ b/weblate/static/js/accounts/profile/index.js @@ -4,17 +4,35 @@ $(document).ready(() => { const $profileNotificationSettings = $("#notifications"); + const $container = $profileNotificationSettings.find("#div_id_watched"); + // Make elements link-like except click behavior + makeElementsLinkLike($container); + // Watch the container when elements are added and removed + const watchedContainerMutationObserver = new MutationObserver(() => { + makeElementsLinkLike($container); + }); - // Open project page on right click - $profileNotificationSettings.on("contextmenu", (e) => { - if ($(e.target).closest(".multi-wrapper a").length > 0) { - const slug = $(e.target).closest(".multi-wrapper a").data("value"); - if (slug) { - window.open(`/projects/${slug}`, "_blank"); - } - // Prevent context menu from displaying - e.preventDefault(); - return true; - } + watchedContainerMutationObserver.observe($container[0], { + childList: true, + subtree: true, }); + + /** + * Iterate over all 'a' elements in parentElement, and if the element has + * 'data-value' attribute, change its `href` to point to project page, and + * prevent default click action. + * + * @param {Object} parentElement - The parent element to search for 'a' + * elements. + */ + function makeElementsLinkLike(parentElement) { + parentElement.find("a").each((_index, element) => { + const $element = $(element); + const dataValue = $element.attr("data-value"); + if (dataValue) { + $element.attr("href", `/projects/${dataValue}`); + $element.on("click", (event) => event.preventDefault()); + } + }); + } }); From 5155e370d4f19ef769530f66e844e1cd1de57766 Mon Sep 17 00:00:00 2001 From: Mehdi El Oualy Date: Fri, 20 Dec 2024 14:10:14 +0100 Subject: [PATCH 5/5] Fix potential XSS vulnerability by encoding user input in href attribute - Added to sanitize the before inserting it into the attribute. - Prevents unsafe HTML/JavaScript injection in URLs and mitigates potential DOM-based XSS attacks. --- weblate/static/js/accounts/profile/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/weblate/static/js/accounts/profile/index.js b/weblate/static/js/accounts/profile/index.js index d82807e42cd1..dec9cdbacf3f 100644 --- a/weblate/static/js/accounts/profile/index.js +++ b/weblate/static/js/accounts/profile/index.js @@ -30,7 +30,9 @@ $(document).ready(() => { const $element = $(element); const dataValue = $element.attr("data-value"); if (dataValue) { - $element.attr("href", `/projects/${dataValue}`); + // Encode the data value to prevent unsafe HTML injection + const safeDataValue = encodeURIComponent(dataValue); // Encode the value + $element.attr("href", `/projects/${safeDataValue}`); $element.on("click", (event) => event.preventDefault()); } });