mirror of
https://github.com/pretix/pretix.git
synced 2025-12-12 04:42:28 +00:00
Compare commits
1 Commits
dialog-bac
...
idna
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e487e35db |
@@ -62,6 +62,7 @@ dependencies = [
|
|||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"geoip2==4.*",
|
||||||
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
|
"idna",
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.2.*",
|
"kombu==5.2.*",
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
'secret': '6zzjnumtsx136ddy',
|
'secret': '6zzjnumtsx136ddy',
|
||||||
'hash': '98kusd8ofsj8dnkd'
|
'hash': '98kusd8ofsj8dnkd'
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import re
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
|
import idna
|
||||||
import markdown
|
import markdown
|
||||||
from bleach import DEFAULT_CALLBACKS
|
from bleach import DEFAULT_CALLBACKS
|
||||||
from bleach.linkifier import build_email_re, build_url_re
|
from bleach.linkifier import build_email_re, build_url_re
|
||||||
@@ -121,6 +122,12 @@ def safelink_callback(attrs, new=False):
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
def idna_decode_safe(src):
|
||||||
|
v = idna.decode(src)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
def truelink_callback(attrs, new=False):
|
def truelink_callback(attrs, new=False):
|
||||||
"""
|
"""
|
||||||
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
|
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
|
||||||
@@ -136,20 +143,40 @@ def truelink_callback(attrs, new=False):
|
|||||||
|
|
||||||
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
||||||
"""
|
"""
|
||||||
text = re.sub(r'[^a-zA-Z0-9.\-/_ ]', '', attrs.get('_text')) # clean up link text
|
text = re.sub(r'[^a-zA-Z0-9:.\-/_ ]', '', attrs.get('_text')) # clean up link text
|
||||||
url = attrs.get((None, 'href'), '/')
|
url = attrs.get((None, 'href'), '/')
|
||||||
href_url = urllib.parse.urlparse(url)
|
href_url = urllib.parse.urlparse(url)
|
||||||
|
strip_http = False
|
||||||
if (None, 'href') in attrs and URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
|
if (None, 'href') in attrs and URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
|
||||||
|
if URL_RE.match(attrs.get('_text').strip()): # maybe we cleaned up too much
|
||||||
|
text = attrs.get('_text').strip()
|
||||||
# link text looks like a url
|
# link text looks like a url
|
||||||
if text.startswith('//'):
|
if text.startswith('//'):
|
||||||
text = 'https:' + text
|
text = 'https:' + text
|
||||||
elif not text.startswith('http'):
|
elif not text.startswith('http'):
|
||||||
|
strip_http = True
|
||||||
text = 'https://' + text
|
text = 'https://' + text
|
||||||
|
|
||||||
text_url = urllib.parse.urlparse(text)
|
text_url = urllib.parse.urlparse(text)
|
||||||
if text_url.netloc != href_url.netloc or not href_url.path.startswith(href_url.path):
|
|
||||||
|
if href_url.netloc.startswith('xn--'):
|
||||||
|
href_netloc_nice = idna_decode_safe(href_url.netloc)
|
||||||
|
else:
|
||||||
|
href_netloc_nice = href_url.netloc
|
||||||
|
|
||||||
|
if text_url.netloc not in (href_url.netloc, href_netloc_nice) or not href_url.path.startswith(text_url.path):
|
||||||
# link text contains an URL that has a different base than the actual URL
|
# link text contains an URL that has a different base than the actual URL
|
||||||
attrs['_text'] = attrs[None, 'href']
|
attrs['_text'] = attrs[None, 'href']
|
||||||
|
text_url = href_url
|
||||||
|
|
||||||
|
if text_url.netloc.startswith('xn--'):
|
||||||
|
# Show punicode nicely
|
||||||
|
text_netloc_nice = idna_decode_safe(text_url.netloc)
|
||||||
|
url = text_url._replace(netloc=text_netloc_nice, scheme=href_url.scheme).geturl()
|
||||||
|
if strip_http:
|
||||||
|
url = url[len(href_url.scheme + '://'):]
|
||||||
|
attrs['_text'] = url
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<div class="helper-space-below">
|
<div class="helper-space-below">
|
||||||
{% trans "Shop URL:" %}
|
{% trans "Shop URL:" %}
|
||||||
<span id="shop_url" class="text-muted">{% abseventurl request.event "presale:event.index" %}</span>
|
<span id="shop_url" class="text-muted">{% humanabseventurl request.event "presale:event.index" %}</span>
|
||||||
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
|
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
|
||||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
|
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<input type="text" name="url"
|
<input type="text" name="url"
|
||||||
value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code|urlencode }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
|
value="{% humanabseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code|urlencode }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="id_url" readonly>
|
id="id_url" readonly>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1513,7 +1513,7 @@ class EventQRCode(EventPermissionRequiredMixin, View):
|
|||||||
permission = 'can_change_event_settings'
|
permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get(self, request, *args, filetype, **kwargs):
|
def get(self, request, *args, filetype, **kwargs):
|
||||||
url = build_absolute_uri(request.event, 'presale:event.index')
|
url = build_absolute_uri(request.event, 'presale:event.index', human_readable=True)
|
||||||
|
|
||||||
qr = qrcode.QRCode(
|
qr = qrcode.QRCode(
|
||||||
version=1,
|
version=1,
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ register = template.Library()
|
|||||||
|
|
||||||
|
|
||||||
class EventURLNode(URLNode):
|
class EventURLNode(URLNode):
|
||||||
def __init__(self, event, view_name, kwargs, asvar, absolute):
|
def __init__(self, event, view_name, kwargs, asvar, absolute, human):
|
||||||
self.event = event
|
self.event = event
|
||||||
self.absolute = absolute
|
self.absolute = absolute
|
||||||
|
self.human = human
|
||||||
super().__init__(view_name, [], kwargs, asvar)
|
super().__init__(view_name, [], kwargs, asvar)
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
@@ -49,7 +50,7 @@ class EventURLNode(URLNode):
|
|||||||
url = ''
|
url = ''
|
||||||
try:
|
try:
|
||||||
if self.absolute:
|
if self.absolute:
|
||||||
url = build_absolute_uri(event, view_name, kwargs=kwargs)
|
url = build_absolute_uri(event, view_name, kwargs=kwargs, human_readable=self.human)
|
||||||
else:
|
else:
|
||||||
url = eventreverse(event, view_name, kwargs=kwargs)
|
url = eventreverse(event, view_name, kwargs=kwargs)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
@@ -66,7 +67,7 @@ class EventURLNode(URLNode):
|
|||||||
|
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def eventurl(parser, token, absolute=False):
|
def eventurl(parser, token, absolute=False, human=False):
|
||||||
"""
|
"""
|
||||||
Similar to {% url %} in the same way that eventreverse() is similar to reverse().
|
Similar to {% url %} in the same way that eventreverse() is similar to reverse().
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ def eventurl(parser, token, absolute=False):
|
|||||||
else:
|
else:
|
||||||
raise TemplateSyntaxError('Event urls only have keyword arguments.')
|
raise TemplateSyntaxError('Event urls only have keyword arguments.')
|
||||||
|
|
||||||
return EventURLNode(event, viewname, kwargs, asvar, absolute)
|
return EventURLNode(event, viewname, kwargs, asvar, absolute, human)
|
||||||
|
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
@@ -106,3 +107,13 @@ def abseventurl(parser, token):
|
|||||||
Returns an absolute URL.
|
Returns an absolute URL.
|
||||||
"""
|
"""
|
||||||
return eventurl(parser, token, absolute=True)
|
return eventurl(parser, token, absolute=True)
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def humanabseventurl(parser, token):
|
||||||
|
"""
|
||||||
|
Similar to {% url %} in the same way that eventreverse() is similar to reverse().
|
||||||
|
|
||||||
|
Returns an absolute URL that is intended to be read by a human.
|
||||||
|
"""
|
||||||
|
return eventurl(parser, token, absolute=True, human=False)
|
||||||
|
|||||||
@@ -32,8 +32,9 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
from urllib.parse import urljoin, urlsplit
|
from urllib.parse import urljoin, urlsplit, urlparse
|
||||||
|
|
||||||
|
import idna
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -171,8 +172,18 @@ def eventreverse(obj, name, kwargs=None):
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def build_absolute_uri(obj, urlname, kwargs=None):
|
def build_absolute_uri(obj, urlname, kwargs=None, human_readable=True):
|
||||||
reversedurl = eventreverse(obj, urlname, kwargs)
|
reversedurl = eventreverse(obj, urlname, kwargs)
|
||||||
if '://' in reversedurl:
|
if '://' in reversedurl:
|
||||||
return reversedurl
|
return reversedurl
|
||||||
return urljoin(settings.SITE_URL, reversedurl)
|
url = urljoin(settings.SITE_URL, reversedurl)
|
||||||
|
|
||||||
|
if human_readable and 'xn--' in url:
|
||||||
|
try:
|
||||||
|
u = urlparse(url)
|
||||||
|
netloc = idna.encode(u.netloc).decode()
|
||||||
|
url = u._replace(netloc=netloc).geturl()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return url
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ def get_public_ical(events):
|
|||||||
event = ev if isinstance(ev, Event) else ev.event
|
event = ev if isinstance(ev, Event) else ev.event
|
||||||
tz = pytz.timezone(event.settings.timezone)
|
tz = pytz.timezone(event.settings.timezone)
|
||||||
if isinstance(ev, Event):
|
if isinstance(ev, Event):
|
||||||
url = build_absolute_uri(event, 'presale:event.index')
|
url = build_absolute_uri(event, 'presale:event.index', human_readable=True)
|
||||||
else:
|
else:
|
||||||
url = build_absolute_uri(event, 'presale:event.index', {
|
url = build_absolute_uri(event, 'presale:event.index', {
|
||||||
'subevent': ev.pk
|
'subevent': ev.pk
|
||||||
})
|
}, human_readable=True)
|
||||||
|
|
||||||
vevent = cal.add('vevent')
|
vevent = cal.add('vevent')
|
||||||
vevent.add('summary').value = str(ev.name)
|
vevent.add('summary').value = str(ev.name)
|
||||||
@@ -122,11 +122,11 @@ def get_private_icals(event, positions):
|
|||||||
evs = set(p.subevent or event for p in positions)
|
evs = set(p.subevent or event for p in positions)
|
||||||
for ev in evs:
|
for ev in evs:
|
||||||
if isinstance(ev, Event):
|
if isinstance(ev, Event):
|
||||||
url = build_absolute_uri(event, 'presale:event.index')
|
url = build_absolute_uri(event, 'presale:event.index', human_readable=True)
|
||||||
else:
|
else:
|
||||||
url = build_absolute_uri(event, 'presale:event.index', {
|
url = build_absolute_uri(event, 'presale:event.index', {
|
||||||
'subevent': ev.pk
|
'subevent': ev.pk
|
||||||
})
|
}, human_readable=True)
|
||||||
|
|
||||||
if event.settings.mail_attach_ical_description:
|
if event.settings.mail_attach_ical_description:
|
||||||
ctx = get_email_context(event=event, event_or_subevent=ev)
|
ctx = get_email_context(event=event, event_or_subevent=ev)
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ from pretix.base.templatetags.rich_text import (
|
|||||||
# Test link detection
|
# Test link detection
|
||||||
("google.com",
|
("google.com",
|
||||||
'<a href="http://google.com" rel="noopener" target="_blank">google.com</a>'),
|
'<a href="http://google.com" rel="noopener" target="_blank">google.com</a>'),
|
||||||
|
("https://google.com",
|
||||||
|
'<a href="https://google.com" rel="noopener" target="_blank">https://google.com</a>'),
|
||||||
|
# Test IDNA conversion
|
||||||
|
("https://xn--dmin-moa0i.com",
|
||||||
|
'<a href="https://xn--dmin-moa0i.com" rel="noopener" target="_blank">https://dömäin.com</a>'),
|
||||||
|
("xn--dmin-moa0i.com",
|
||||||
|
'<a href="http://xn--dmin-moa0i.com" rel="noopener" target="_blank">dömäin.com</a>'),
|
||||||
|
("[dömäin.com](https://xn--dmin-moa0i.com)",
|
||||||
|
'<a href="https://xn--dmin-moa0i.com" rel="noopener" target="_blank">dömäin.com</a>'),
|
||||||
|
("https://dömäin.com",
|
||||||
|
'<a href="https://dömäin.com" rel="noopener" target="_blank">https://dömäin.com</a>'),
|
||||||
|
("[https://dömäin.com](https://xn--dmin-moa0i.com)",
|
||||||
|
'<a href="https://xn--dmin-moa0i.com" rel="noopener" target="_blank">https://dömäin.com</a>'),
|
||||||
|
("[xn--dmin-moa0i.com](https://xn--dmin-moa0i.com)",
|
||||||
|
'<a href="https://xn--dmin-moa0i.com" rel="noopener" target="_blank">dömäin.com</a>'),
|
||||||
# Test abslink_callback
|
# Test abslink_callback
|
||||||
("[Call](tel:+12345)",
|
("[Call](tel:+12345)",
|
||||||
'<a href="tel:+12345" rel="nofollow">Call</a>'),
|
'<a href="tel:+12345" rel="nofollow">Call</a>'),
|
||||||
@@ -54,6 +69,10 @@ from pretix.base.templatetags.rich_text import (
|
|||||||
'<a href="https://goodsite.com.evilsite.com" rel="noopener" target="_blank">https://goodsite.com.evilsite.com</a>'),
|
'<a href="https://goodsite.com.evilsite.com" rel="noopener" target="_blank">https://goodsite.com.evilsite.com</a>'),
|
||||||
('<a href="https://evilsite.com/deep/path">evilsite.com</a>',
|
('<a href="https://evilsite.com/deep/path">evilsite.com</a>',
|
||||||
'<a href="https://evilsite.com/deep/path" rel="noopener" target="_blank">evilsite.com</a>'),
|
'<a href="https://evilsite.com/deep/path" rel="noopener" target="_blank">evilsite.com</a>'),
|
||||||
|
('<a href="https://evilsite.com/deep/path">evilsite.com/deep</a>',
|
||||||
|
'<a href="https://evilsite.com/deep/path" rel="noopener" target="_blank">evilsite.com/deep</a>'),
|
||||||
|
('<a href="https://evilsite.com/deep/path">evilsite.com/other</a>',
|
||||||
|
'<a href="https://evilsite.com/deep/path" rel="noopener" target="_blank">https://evilsite.com/deep/path</a>'),
|
||||||
('<a>broken</a>', '<a>broken</a>'),
|
('<a>broken</a>', '<a>broken</a>'),
|
||||||
])
|
])
|
||||||
def test_linkify_abs(link):
|
def test_linkify_abs(link):
|
||||||
|
|||||||
Reference in New Issue
Block a user