From b8d5088b6222dc465c4c1eacafa2e60ee14f7bc6 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Wed, 15 Sep 2021 16:40:25 +0200
Subject: [PATCH] pwa: allow absolute redirection url from internal page
 (#56974)

---
 combo/apps/pwa/models.py                      | 10 ++++-
 .../pwa/templates/combo/pwa/navigation.html   |  4 +-
 tests/test_pwa.py                             | 41 +++++++++++++++++++
 3 files changed, 51 insertions(+), 4 deletions(-)

diff --git a/combo/apps/pwa/models.py b/combo/apps/pwa/models.py
index ff0c84dc..564a8a2a 100644
--- a/combo/apps/pwa/models.py
+++ b/combo/apps/pwa/models.py
@@ -13,29 +13,31 @@
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import base64
 import json
 import os
+import urllib.parse
 
 from django.conf import settings
 from django.contrib.postgres.fields import JSONField
 from django.core import serializers
 from django.core.files.base import ContentFile
 from django.core.files.storage import default_storage
 from django.db import models
 from django.utils.encoding import force_bytes, force_text
 from django.utils.translation import ugettext_lazy as _
 from py_vapid import Vapid
 
 from combo import utils
 from combo.data.fields import RichTextField
+from combo.middleware import get_request
 
 
 class PwaSettings(models.Model):
     APPLICATION_ICON_SIZES = ['%sx%s' % (x, x) for x in (48, 96, 192, 256, 512)]
     application_name = models.CharField(verbose_name=_('Application Name'), max_length=64, blank=True)
     application_icon = models.FileField(
         verbose_name=_('Application Icon'),
         help_text=_(
@@ -132,19 +134,23 @@ class PwaNavigationEntry(models.Model):
     class Meta:
         ordering = ('order',)
 
     def get_label(self):
         return self.label or self.link_page.title
 
     def get_url(self):
         if self.link_page:
-            return self.link_page.get_online_url()
+            url = self.link_page.get_online_url()
         else:
-            return utils.get_templated_url(self.url)
+            url = utils.get_templated_url(self.url)
+        if not urllib.parse.urlparse(url).netloc and get_request():
+            site_base = get_request().build_absolute_uri()
+            url = site_base + url.lstrip('/')
+        return url
 
     def css_class_names(self):
         css_class_names = self.extra_css_class or ''
         if self.link_page:
             css_class_names += ' page-%s' % self.link_page.slug
         return css_class_names
 
     @classmethod
diff --git a/combo/apps/pwa/templates/combo/pwa/navigation.html b/combo/apps/pwa/templates/combo/pwa/navigation.html
index 3ce98275..2fcb108e 100644
--- a/combo/apps/pwa/templates/combo/pwa/navigation.html
+++ b/combo/apps/pwa/templates/combo/pwa/navigation.html
@@ -2,17 +2,17 @@
 <div class="pwa-navigation" id="pwa-navigation"
   {% if include_user_name %}data-pwa-user-name="{% skeleton_extra_placeholder user-name %}{{user.get_full_name}}{% end_skeleton_extra_placeholder %}"{% endif %}>
 <div>
 <ul>
   {% for entry in entries %}
   <li class="{{ entry.css_class_names }}{% if entry.link_page in page.get_parents_and_self  %} selected{% endif %}" data-entry-pk="{{ entry.pk }}"
       {% if entry.notification_count %}data-notification-count-url="{{site_base}}/api/notification/count/"{% endif %}
       {% if entry.use_user_name_as_label %}data-include-user-name{% endif %}>
-      <a href="{% if entry.link_page_id %}{{ site_base }}{% endif %}{{ entry.get_url }}"
+      <a href="{{ entry.get_url }}"
          {% if entry.icon %}style="background-image: url({{site_base}}{{entry.icon.url}});"{% endif %}
          ><span>{{ entry.get_label }}</span></a></li>
   {% endfor %}
 </ul>
 </div>
 </div>
 <script>
 $('li[data-include-user-name]').each(function(idx, elem) {
@@ -31,9 +31,9 @@ $('body.authenticated-user li[data-notification-count-url]').each(function(idx,
     crossDomain: true,
     success: function(data) {
       if (data.new) {
         $entry.find('span').append(' <span class="badge">' + data.new + '</span>');
       }
   }});
 });
 </script>
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/tests/test_pwa.py b/tests/test_pwa.py
index 3adf0832..78654692 100644
--- a/tests/test_pwa.py
+++ b/tests/test_pwa.py
@@ -1,9 +1,10 @@
 import base64
+from html.parser import HTMLParser
 from unittest import mock
 
 import pytest
 from django.conf import settings
 from django.core.files import File
 from django.template import Context, Template
 from django.test import override_settings
 from django.test.client import RequestFactory
@@ -248,16 +249,56 @@ def test_pwa_navigation_templatetag(app):
         assert nav.count('background-image') == 1
         assert nav.count('data-notification-count-url=') == 1
         assert nav.count('data-pwa-user-name=""') == 1
 
         nav = t.render(Context({'request': request, 'render_skeleton': True}))
         assert 'data-pwa-user-name="{% block placeholder-user-name %}' in nav
 
 
+def test_pwa_navigation_templatetag_internal_redirection(app):
+    page1 = Page(title='One', slug='one')
+    page2 = Page(title='Two', slug='two', redirect_url='http://www.example.org/test')
+    page3 = Page(title='Three', slug='three', redirect_url='../test')
+    page4 = Page(title='Four', slug='four', redirect_url='{{test_url}}plop')
+    page1.save()
+    page2.save()
+    page3.save()
+    page4.save()
+    entry1 = PwaNavigationEntry(link_page=page1, order=1)
+    entry2 = PwaNavigationEntry(link_page=page2, order=2)
+    entry3 = PwaNavigationEntry(link_page=page3, order=3)
+    entry4 = PwaNavigationEntry(link_page=page4, order=4)
+    entry1.save()
+    entry2.save()
+    entry3.save()
+    entry4.save()
+    t = Template('{% load pwa %}{% pwa_navigation %}')
+
+    with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
+        request = RequestFactory().get('/')
+        with mock.patch('combo.apps.pwa.models.get_request', return_value=request):
+            nav = t.render(Context({'request': request}))
+
+    class MyHTMLParser(HTMLParser):
+        def handle_starttag(self, tag, attrs):
+            if tag == 'a':
+                results.append(attrs[0][1])
+
+    parser = MyHTMLParser()
+    results = []
+    parser.feed(nav)
+    assert results == [
+        'http://testserver/one/',
+        'http://www.example.org/test',
+        'http://testserver/three/',
+        'http://testserver/four/',
+    ]
+
+
 def test_pwa_application_icon(app, admin_user):
     app = login(app)
     with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
         PwaSettings.objects.all().delete()
         resp = app.get('/manage/pwa/')
         resp.form['application_icon'] = Upload(
             'test.png',
             base64.decodebytes(
-- 
2.30.2

