55import os
66import re
77from pyquery import PyQuery
8- from urllib .parse import urlparse
8+ from urllib .parse import urlparse , urlsplit
99
1010
1111from django .conf import settings
1212from django .core .exceptions import ValidationError
13- from django .core .validators import RegexValidator , URLValidator , EmailValidator
13+ from django .core .validators import RegexValidator , URLValidator , EmailValidator , _lazy_re_compile
1414from django .template .defaultfilters import filesizeformat
1515from django .utils .deconstruct import deconstructible
16+ from django .utils .translation import gettext_lazy as _
1617
1718import debug # pyflakes:ignore
1819
@@ -92,6 +93,75 @@ def validate_no_html_frame(file):
9293validate_http_url = URLValidator (schemes = ['http' ,'https' ])
9394validate_email = EmailValidator ()
9495
96+ @deconstructible
97+ class XMPPURLValidator (RegexValidator ):
98+ ul = '\u00a1 -\uffff ' # unicode letters range (must not be a raw string)
99+
100+ # IP patterns
101+ ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
102+ ipv6_re = r'\[[0-9a-f:\.]+\]' # (simple regex, validated later)
103+
104+ # Host patterns
105+ hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
106+ # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
107+ domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
108+ tld_re = (
109+ r'\.' # dot
110+ r'(?!-)' # can't start with a dash
111+ r'(?:[a-z' + ul + '-]{2,63}' # domain label
112+ r'|xn--[a-z0-9]{1,59})' # or punycode label
113+ r'(?<!-)' # can't end with a dash
114+ r'\.?' # may have a trailing dot
115+ )
116+ host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'
117+
118+ regex = _lazy_re_compile (
119+ r'^(?:xmpp:)' # Note there is no '//'
120+ r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # user:pass authentication
121+ r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
122+ r'(?::\d{2,5})?' # port
123+ r'(?:[/?#][^\s]*)?' # resource path
124+ r'\Z' , re .IGNORECASE )
125+ message = _ ('Enter a valid URL.' )
126+ schemes = ['http' , 'https' , 'ftp' , 'ftps' ]
127+
128+ def __call__ (self , value ):
129+ try :
130+ super ().__call__ (value )
131+ except ValidationError as e :
132+ # Trivial case failed. Try for possible IDN domain
133+ if value :
134+ try :
135+ scheme , netloc , path , query , fragment = urlsplit (value )
136+ except ValueError : # for example, "Invalid IPv6 URL"
137+ raise ValidationError (self .message , code = self .code )
138+ try :
139+ netloc = netloc .encode ('idna' ).decode ('ascii' ) # IDN -> ACE
140+ except UnicodeError : # invalid domain part
141+ raise e
142+ url = urlunsplit ((scheme , netloc , path , query , fragment ))
143+ super ().__call__ (url )
144+ else :
145+ raise
146+ else :
147+ # Now verify IPv6 in the netloc part
148+ host_match = re .search (r'^\[(.+)\](?::\d{2,5})?$' , urlsplit (value ).netloc )
149+ if host_match :
150+ potential_ip = host_match .groups ()[0 ]
151+ try :
152+ validate_ipv6_address (potential_ip )
153+ except ValidationError :
154+ raise ValidationError (self .message , code = self .code )
155+
156+ # The maximum length of a full host name is 253 characters per RFC 1034
157+ # section 3.1. It's defined to be 255 bytes or less, but this includes
158+ # one byte for the length of the name and one byte for the trailing dot
159+ # that's used to indicate absolute names in DNS.
160+ if len (urlsplit (value ).netloc ) > 253 :
161+ raise ValidationError (self .message , code = self .code )
162+
163+ validate_xmpp = XMPPURLValidator ()
164+
95165def validate_external_resource_value (name , value ):
96166 """ validate a resource value using its name's properties """
97167
@@ -102,11 +172,7 @@ def validate_external_resource_value(name, value):
102172 if urlparse (value ).netloc .lower () != 'github.com' :
103173 raise ValidationError ('URL must be a github url' )
104174 elif name .slug == 'jabber_room' :
105- pass
106- # TODO - build a xmpp URL validator. See XEP-0032.
107- # It should be easy to build one by copyhacking URLValidator,
108- # but reading source says it would be better to wait to do that
109- # until after we make the Django 2 transition
175+ validate_xmpp (value )
110176 else :
111177 validate_url (value )
112178
0 commit comments