Code

Ticket #811: ipv6-standalone.diff

File ipv6-standalone.diff, 16.5 KB (added by Johann Queuniet <johann.queuniet@…>, 6 years ago)

Same as the 02/02/08 patch, without IPy

Line 
1Index: django/oldforms/__init__.py
2===================================================================
3--- django/oldforms/__init__.py (revision 7457)
4+++ django/oldforms/__init__.py (working copy)
5@@ -933,7 +933,7 @@
6 
7     def isValidIPAddress(self, field_data, all_data):
8         try:
9-            validators.isValidIPAddress4(field_data, all_data)
10+            validators.isValidIP4Address(field_data, all_data)
11         except validators.ValidationError, e:
12             raise validators.CriticalValidationError, e.messages
13 
14@@ -941,6 +941,23 @@
15         return data or None
16     html2python = staticmethod(html2python)
17 
18+class IP6AddressField(IPAddressField):
19+    def __init__(self, field_name, length=39, max_length=39, is_required=False, validator_list=None):
20+        if validator_list is None: validator_list = []
21+        validator_list = [self.isValidIPAddress] + validator_list
22+        TextField.__init__(self, field_name, length=length, max_length=max_length,
23+            is_required=is_required, validator_list=validator_list)
24+
25+    def isValidIPAddress(self, field_data, all_data):
26+        try:
27+            validators.isValidIPAddress(field_data, all_data)
28+        except validators.ValidationError, e:
29+            raise validators.CriticalValidationError, e.messages
30+
31+    def html2python(data):
32+        return data or None
33+    html2python = staticmethod(html2python)
34+
35 ####################
36 # MISCELLANEOUS    #
37 ####################
38Index: django/db/models/fields/__init__.py
39===================================================================
40--- django/db/models/fields/__init__.py (revision 7457)
41+++ django/db/models/fields/__init__.py (working copy)
42@@ -943,6 +943,24 @@
43         defaults.update(kwargs)
44         return super(IPAddressField, self).formfield(**defaults)
45 
46+class IP6AddressField(Field):
47+    empty_strings_allowed = False
48+    def __init__(self, *args, **kwargs):
49+        kwargs['max_length'] = 39
50+        Field.__init__(self, *args, **kwargs)
51+
52+    def get_manipulator_field_objs(self):
53+        return [oldforms.IP6AddressField]
54+
55+    def validate(self, field_data, all_data):
56+        validators.isValidIPAddress(field_data, None)
57+
58+    def formfield(self, **kwargs):
59+        defaults = {'form_class': forms.IP6AddressField}
60+        defaults.update(kwargs)
61+        return super(IP6AddressField, self).formfield(**defaults)
62+
63+
64 class NullBooleanField(Field):
65     empty_strings_allowed = False
66     def __init__(self, *args, **kwargs):
67Index: django/db/backends/postgresql/introspection.py
68===================================================================
69--- django/db/backends/postgresql/introspection.py      (revision 7457)
70+++ django/db/backends/postgresql/introspection.py      (working copy)
71@@ -75,7 +75,7 @@
72     23: 'IntegerField',
73     25: 'TextField',
74     701: 'FloatField',
75-    869: 'IPAddressField',
76+    869: 'IP6AddressField',
77     1043: 'CharField',
78     1082: 'DateField',
79     1083: 'TimeField',
80Index: django/db/backends/postgresql/creation.py
81===================================================================
82--- django/db/backends/postgresql/creation.py   (revision 7457)
83+++ django/db/backends/postgresql/creation.py   (working copy)
84@@ -16,6 +16,7 @@
85     'ImageField':        'varchar(%(max_length)s)',
86     'IntegerField':      'integer',
87     'IPAddressField':    'inet',
88+    'IP6AddressField':   'inet',
89     'NullBooleanField':  'boolean',
90     'OneToOneField':     'integer',
91     'PhoneNumberField':  'varchar(20)',
92Index: django/db/backends/mysql_old/creation.py
93===================================================================
94--- django/db/backends/mysql_old/creation.py    (revision 7457)
95+++ django/db/backends/mysql_old/creation.py    (working copy)
96@@ -16,6 +16,7 @@
97     'ImageField':        'varchar(%(max_length)s)',
98     'IntegerField':      'integer',
99     'IPAddressField':    'char(15)',
100+    'IP6AddressField':   'char(39)',
101     'NullBooleanField':  'bool',
102     'OneToOneField':     'integer',
103     'PhoneNumberField':  'varchar(20)',
104Index: django/db/backends/sqlite3/creation.py
105===================================================================
106--- django/db/backends/sqlite3/creation.py      (revision 7457)
107+++ django/db/backends/sqlite3/creation.py      (working copy)
108@@ -15,6 +15,7 @@
109     'ImageField':                   'varchar(%(max_length)s)',
110     'IntegerField':                 'integer',
111     'IPAddressField':               'char(15)',
112+    'IP6AddressField':              'char(39)',
113     'NullBooleanField':             'bool',
114     'OneToOneField':                'integer',
115     'PhoneNumberField':             'varchar(20)',
116Index: django/db/backends/mysql/creation.py
117===================================================================
118--- django/db/backends/mysql/creation.py        (revision 7457)
119+++ django/db/backends/mysql/creation.py        (working copy)
120@@ -16,6 +16,7 @@
121     'ImageField':        'varchar(%(max_length)s)',
122     'IntegerField':      'integer',
123     'IPAddressField':    'char(15)',
124+    'IPAddressField':    'char(39)',
125     'NullBooleanField':  'bool',
126     'OneToOneField':     'integer',
127     'PhoneNumberField':  'varchar(20)',
128Index: django/db/backends/oracle/creation.py
129===================================================================
130--- django/db/backends/oracle/creation.py       (revision 7457)
131+++ django/db/backends/oracle/creation.py       (working copy)
132@@ -19,6 +19,7 @@
133     'ImageField':                   'NVARCHAR2(%(max_length)s)',
134     'IntegerField':                 'NUMBER(11)',
135     'IPAddressField':               'VARCHAR2(15)',
136+    'IP6AddressField':              'VARCHAR2(39)',
137     'NullBooleanField':             'NUMBER(1) CHECK ((%(column)s IN (0,1)) OR (%(column)s IS NULL))',
138     'OneToOneField':                'NUMBER(11)',
139     'PhoneNumberField':             'VARCHAR2(20)',
140Index: django/db/backends/postgresql_psycopg2/introspection.py
141===================================================================
142--- django/db/backends/postgresql_psycopg2/introspection.py     (revision 7457)
143+++ django/db/backends/postgresql_psycopg2/introspection.py     (working copy)
144@@ -72,7 +72,7 @@
145     23: 'IntegerField',
146     25: 'TextField',
147     701: 'FloatField',
148-    869: 'IPAddressField',
149+    869: 'IP6AddressField',
150     1043: 'CharField',
151     1082: 'DateField',
152     1083: 'TimeField',
153Index: django/core/validators.py
154===================================================================
155--- django/core/validators.py   (revision 7457)
156+++ django/core/validators.py   (working copy)
157@@ -19,6 +19,7 @@
158 from django.utils.translation import ugettext as _, ugettext_lazy, ungettext
159 from django.utils.functional import Promise, lazy
160 from django.utils.encoding import force_unicode, smart_str
161+from django.utils.http import ip6_normalize
162 
163 _datere = r'\d{4}-\d{1,2}-\d{1,2}'
164 _timere = r'(?:[01]?[0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?'
165@@ -33,6 +34,7 @@
166     r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
167 integer_re = re.compile(r'^-?\d+$')
168 ip4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
169+ip6_re = re.compile(r'^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$')
170 phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNORECASE)
171 slug_re = re.compile(r'^[-\w]+$')
172 url_re = re.compile(r'^https?://\S+$')
173@@ -106,10 +108,19 @@
174         except ValidationError:
175             raise ValidationError, _("Enter valid e-mail addresses separated by commas.")
176 
177-def isValidIPAddress4(field_data, all_data):
178+def isValidIP4Address(field_data, all_data):
179     if not ip4_re.search(field_data):
180-        raise ValidationError, _("Please enter a valid IP address.")
181+        raise ValidationError, _("Please enter a valid IPv4 address.")
182 
183+def isValidIPAddress(field_data, all_data):
184+    if not ip4_re.search(field_data):
185+        try:
186+            ip = ip6_normalize(field_data)
187+            if not ip6_re.search(ip):
188+                raise ValidationError, _("Please enter a valid IP address.")
189+        except ValueError:
190+            raise ValidationError, _("Please enter a valid IP address.")
191+
192 def isNotEmpty(field_data, all_data):
193     if field_data.strip() == '':
194         raise ValidationError, _("Empty values are not allowed here.")
195Index: django/newforms/fields.py
196===================================================================
197--- django/newforms/fields.py   (revision 7457)
198+++ django/newforms/fields.py   (working copy)
199@@ -19,6 +19,7 @@
200 
201 from django.utils.translation import ugettext_lazy as _
202 from django.utils.encoding import StrAndUnicode, smart_unicode, smart_str
203+from django.utils.http import ip6_normalize
204 
205 from util import ErrorList, ValidationError
206 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput
207@@ -32,7 +33,7 @@
208     'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField',
209     'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
210     'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
211-    'SplitDateTimeField', 'IPAddressField', 'FilePathField',
212+    'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'IP6AddressField',
213 )
214 
215 # These values, if given to to_python(), will trigger the self.required check.
216@@ -782,3 +783,22 @@
217 
218     def __init__(self, *args, **kwargs):
219         super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs)
220+
221+ipv6_re = re.compile(r'^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$')
222+
223+class IP6AddressField(CharField):
224+    def __init__(self, *args, **kwargs):
225+        super(IP6AddressField, self).__init__(39, 3, *args, **kwargs)
226+
227+    def clean(self, value):
228+        value = super(IP6AddressField, self).clean(value)
229+        if value == u'':
230+            return value
231+        if not ipv4_re.search(value):
232+            try:
233+                ip = ip6_normalize(value)
234+                if not ipv6_re.search(ip):
235+                    raise ValidationError(_(u'Enter a valid IP address.'))
236+            except ValueError:
237+                raise ValidationError(_(u'Enter a valid IP address.'))
238+        return value
239Index: django/utils/http.py
240===================================================================
241--- django/utils/http.py        (revision 7457)
242+++ django/utils/http.py        (working copy)
243@@ -65,3 +65,54 @@
244     """
245     rfcdate = formatdate(epoch_seconds)
246     return '%s GMT' % rfcdate[:25]
247+
248+def ip6_normalize(addr):
249+    """
250+    Normalize an IPv6 address to allow easy regexp validation
251+    mostly checks the length, and gets ride of tricky things
252+    like IPv4 mapped addresses and :: shortcuts
253+   
254+    Outputs a string
255+    """
256+    # Some basic error checking
257+    if addr.count('::') > 2 or ':::' in addr:
258+        raise ValueError
259+
260+    ip = addr.split(':')
261+    nbfull = len([elem for elem in ip if elem != ''])
262+    nb = len(ip)
263+
264+    if nbfull >= 1 and '.' in ip[-1]:
265+        # Convert IPv4 mapped addresses to full hexa
266+        ipv4 = ip[-1].split('.')
267+        hex1 = (int(ipv4[0]) << 8) + int(ipv4[1])
268+        hex2 = (int(ipv4[2]) << 8) + int(ipv4[3])
269+        ip[-1:] = [hex(hex1)[2:], hex(hex2)[2:]]
270+        nbfull = nbfull + 1
271+        nb = nb + 1
272+
273+    if nbfull == 8 or nbfull == nb:
274+        # No need to bother
275+        return addr
276+    elif nbfull > 8:
277+        # Has to be invalid anyway
278+        raise ValueError
279+
280+    # Begin normalization
281+    start, end, index = (None, None, 0)
282+    for elem in ip:
283+        if elem == '':
284+            if start is None:
285+                start = index
286+                end = index
287+            else:
288+                end = index
289+        index += 1
290+    pad = 8 - nbfull
291+    if end != start:
292+        ip[start:end-start+1] = ['0'] * pad
293+    else:
294+        ip[start] = '0'
295+        if pad > 1:
296+            ip[start:1] = ['0'] * (pad - 1)
297+    return ':'.join([item for item in ip if len(item) > 0])
298Index: tests/regressiontests/forms/extra.py
299===================================================================
300--- tests/regressiontests/forms/extra.py        (revision 7457)
301+++ tests/regressiontests/forms/extra.py        (working copy)
302@@ -377,6 +377,85 @@
303 ...
304 ValidationError: [u'Enter a valid IPv4 address.']
305 
306+# IP6AddressField ##################################################################
307+
308+>>> f = IP6AddressField()
309+>>> f.clean('')
310+Traceback (most recent call last):
311+...
312+ValidationError: [u'This field is required.']
313+>>> f.clean(None)
314+Traceback (most recent call last):
315+...
316+ValidationError: [u'This field is required.']
317+>>> f.clean('127.0.0.1')
318+u'127.0.0.1'
319+>>> f.clean('2a01:05d8:25ae:9451:020d:39bc:21e6:8cab')
320+u'2a01:05d8:25ae:9451:020d:39bc:21e6:8cab'
321+>>> f.clean('2a01:5d8:25ae:9451:20d:39bc:21e6:8cab')
322+u'2a01:5d8:25ae:9451:20d:39bc:21e6:8cab'
323+>>> f.clean('::1')
324+u'::1'
325+>>> f.clean('::ffff:88.191.32.1')
326+u'::ffff:88.191.32.1'
327+>>> f.clean('foo')
328+Traceback (most recent call last):
329+...
330+ValidationError: [u'Enter a valid IP address.']
331+>>> f.clean('127.0.0.')
332+Traceback (most recent call last):
333+...
334+ValidationError: [u'Enter a valid IP address.']
335+>>> f.clean('::1:')
336+Traceback (most recent call last):
337+...
338+ValidationError: [u'Enter a valid IP address.']
339+>>> f.clean('1.2.3.4.5')
340+Traceback (most recent call last):
341+...
342+ValidationError: [u'Enter a valid IP address.']
343+>>> f.clean('2a01:5d8:25ae:9451:20d:39bc:21e6:8cab:fb2c')
344+Traceback (most recent call last):
345+...
346+ValidationError: [u'Ensure this value has at most 39 characters (it has 42).']
347+>>> f.clean('2a01:5d8:25ae:9451:20d:39bc:1e6:cab:b2c')
348+Traceback (most recent call last):
349+...
350+ValidationError: [u'Enter a valid IP address.']
351+>>> f.clean('256.125.1.5')
352+Traceback (most recent call last):
353+...
354+ValidationError: [u'Enter a valid IP address.']
355+>>> f.clean('::12345')
356+Traceback (most recent call last):
357+...
358+ValidationError: [u'Enter a valid IP address.']
359+
360+>>> f = IP6AddressField(required=False)
361+>>> f.clean('')
362+u''
363+>>> f.clean(None)
364+u''
365+>>> f.clean('127.0.0.1')
366+u'127.0.0.1'
367+>>> f.clean('foo')
368+Traceback (most recent call last):
369+...
370+ValidationError: [u'Enter a valid IP address.']
371+>>> f.clean('127.0.0.')
372+Traceback (most recent call last):
373+...
374+ValidationError: [u'Enter a valid IP address.']
375+>>> f.clean('1.2.3.4.5')
376+Traceback (most recent call last):
377+...
378+ValidationError: [u'Enter a valid IP address.']
379+>>> f.clean('256.125.1.5')
380+Traceback (most recent call last):
381+...
382+ValidationError: [u'Enter a valid IP address.']
383+
384+
385 #################################
386 # Tests of underlying functions #
387 #################################
388Index: docs/modelforms.txt
389===================================================================
390--- docs/modelforms.txt (revision 7457)
391+++ docs/modelforms.txt (working copy)
392@@ -58,6 +58,7 @@
393     ``ImageField``                   ``ImageField``
394     ``IntegerField``                 ``IntegerField``
395     ``IPAddressField``               ``IPAddressField``
396+    ``IP6AddressField``               ``IP6AddressField``
397     ``ManyToManyField``              ``ModelMultipleChoiceField`` (see
398                                      below)
399     ``NullBooleanField``             ``CharField``
400Index: docs/model-api.txt
401===================================================================
402--- docs/model-api.txt  (revision 7457)
403+++ docs/model-api.txt  (working copy)
404@@ -388,6 +388,14 @@
405 
406 The admin represents this as an ``<input type="text">`` (a single-line input).
407 
408+``IP6AddressField``
409+~~~~~~~~~~~~~~~~~~
410+
411+An IPv6 or IPv4 address, in string format (i.e. "24.124.1.30",
412+"2002:58bf:278c::1", "::ffff:88.191.52.1").
413+
414+The admin represents this as an ``<input type="text">`` (a single-line input).
415+
416 ``NullBooleanField``
417 ~~~~~~~~~~~~~~~~~~~~
418 
419Index: docs/newforms.txt
420===================================================================
421--- docs/newforms.txt   (revision 7457)
422+++ docs/newforms.txt   (working copy)
423@@ -1427,6 +1427,15 @@
424       expression.
425     * Error message keys: ``required``, ``invalid``
426 
427+``IP6AddressField``
428+~~~~~~~~~~~~~~~~~~
429+
430+    * Default widget: ``TextInput``
431+    * Empty value: ``''`` (an empty string)
432+    * Normalizes to: A Unicode object.
433+    * Validates that the given value is a valid IPv4 or IPv6 address
434+    * Error message keys: ``required``, ``invalid``
435+
436 ``MultipleChoiceField``
437 ~~~~~~~~~~~~~~~~~~~~~~~
438 
439Index: docs/form_for_model.txt
440===================================================================
441--- docs/form_for_model.txt     (revision 7457)
442+++ docs/form_for_model.txt     (working copy)
443@@ -65,6 +65,7 @@
444     ``ImageField``                   ``ImageField``
445     ``IntegerField``                 ``IntegerField``
446     ``IPAddressField``               ``IPAddressField``
447+    ``IP6AddressField``               ``IP6AddressField``
448     ``ManyToManyField``              ``ModelMultipleChoiceField`` (see
449                                      below)
450     ``NullBooleanField``             ``CharField``