1# -*- coding: utf-8 -*-
2
3# Placed into the Public Domain by tav <tav@espians.com>
4# Modified for Python 3 compatibility.
5
6"Validate Javascript Identifiers for use as JSON-P callback parameters."
7from __future__ import unicode_literals
8import re
9
10from unicodedata import category
11
12import six
13
14# -----------------------------------------------------------------------------
15# javascript identifier unicode categories and "exceptional" chars
16# -----------------------------------------------------------------------------
17
18valid_jsid_categories_start = frozenset([
19    'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl'
20])
21
22valid_jsid_categories = frozenset([
23    'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc'
24])
25
26valid_jsid_chars = ('$', '_')
27
28# -----------------------------------------------------------------------------
29# regex to find array[index] patterns
30# -----------------------------------------------------------------------------
31
32array_index_regex = re.compile(r'\[[0-9]+\]$')
33
34has_valid_array_index = array_index_regex.search
35replace_array_index = array_index_regex.sub
36
37# -----------------------------------------------------------------------------
38# javascript reserved words -- including keywords and null/boolean literals
39# -----------------------------------------------------------------------------
40
41is_reserved_js_word = frozenset([
42    'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class',
43    'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double',
44    'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float',
45    'for', 'function', 'goto', 'if', 'implements', 'import', 'in',
46    'instanceof', 'int', 'interface', 'long', 'native', 'new', 'null',
47    'package', 'private', 'protected', 'public', 'return', 'short', 'static',
48    'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient',
49    'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with',
50
51    # potentially reserved in a future version of the ES5 standard
52    # 'let', 'yield'
53
54]).__contains__
55
56# -----------------------------------------------------------------------------
57# the core validation functions
58# -----------------------------------------------------------------------------
59
60
61def is_valid_javascript_identifier(identifier, escape=r'\\u',
62        ucd_cat=category):
63    """Return whether the given ``id`` is a valid Javascript identifier."""
64
65    if not identifier:
66        return False
67
68    if not isinstance(identifier, six.text_type):
69        try:
70            identifier = six.text_type(identifier, 'utf-8')
71        except UnicodeDecodeError:
72            return False
73
74    if escape in identifier:
75        new = []
76        add_char = new.append
77        split_id = identifier.split(escape)
78        add_char(split_id.pop(0))
79
80        for segment in split_id:
81            if len(segment) < 4:
82                return False
83            try:
84                add_char(six.unichr(int('0x' + segment[:4], 16)))
85            except Exception:
86                return False
87            add_char(segment[4:])
88
89        identifier = u''.join(new)
90
91    if is_reserved_js_word(identifier):
92        return False
93
94    first_char = identifier[0]
95
96    if not ((first_char in valid_jsid_chars)
97            or (ucd_cat(first_char) in valid_jsid_categories_start)):
98        return False
99
100    for char in identifier[1:]:
101        if not ((char in valid_jsid_chars)
102                or (ucd_cat(char) in valid_jsid_categories)):
103            return False
104
105    return True
106
107
108def is_valid_jsonp_callback_value(value):
109    "Return whether the given ``value`` can be used as a JSON-P callback."
110
111    for identifier in value.split(u'.'):
112        while '[' in identifier:
113            if not has_valid_array_index(identifier):
114                return False
115            identifier = replace_array_index(u'', identifier)
116        if not is_valid_javascript_identifier(identifier):
117            return False
118
119    return True
120
121# -----------------------------------------------------------------------------
122# test
123# -----------------------------------------------------------------------------
124
125
126def test():
127    """
128    The function ``is_valid_javascript_identifier`` validates a given
129    identifier according to the latest draft of the ECMAScript 5 Specification:
130
131      >>> is_valid_javascript_identifier('hello')
132      True
133
134      >>> is_valid_javascript_identifier('alert()')
135      False
136
137      >>> is_valid_javascript_identifier('a-b')
138      False
139
140      >>> is_valid_javascript_identifier('23foo')
141      False
142
143      >>> is_valid_javascript_identifier('foo23')
144      True
145
146      >>> is_valid_javascript_identifier('$210')
147      True
148
149      >>> is_valid_javascript_identifier(u'Stra\u00dfe')
150      True
151
152      >>> is_valid_javascript_identifier(r'\u0062') # u'b'
153      True
154
155      >>> is_valid_javascript_identifier(r'\u0020')
156      False
157
158      >>> is_valid_javascript_identifier('_bar')
159      True
160
161      >>> is_valid_javascript_identifier('some_var')
162      True
163
164      >>> is_valid_javascript_identifier('$')
165      True
166
167    But ``is_valid_jsonp_callback_value`` is the function you want to use for
168    validating JSON-P callback parameter values:
169
170      >>> is_valid_jsonp_callback_value('somevar')
171      True
172
173      >>> is_valid_jsonp_callback_value('function')
174      False
175
176      >>> is_valid_jsonp_callback_value(' somevar')
177      False
178
179    It supports the possibility of '.' being present in the callback name, e.g.
180
181      >>> is_valid_jsonp_callback_value('$.ajaxHandler')
182      True
183
184      >>> is_valid_jsonp_callback_value('$.23')
185      False
186
187    As well as the pattern of providing an array index lookup, e.g.
188
189      >>> is_valid_jsonp_callback_value('array_of_functions[42]')
190      True
191
192      >>> is_valid_jsonp_callback_value('array_of_functions[42][1]')
193      True
194
195      >>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo')
196      True
197
198      >>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]')
199      False
200
201      >>> is_valid_jsonp_callback_value('array_of_functions[]')
202      False
203
204      >>> is_valid_jsonp_callback_value('array_of_functions["key"]')
205      False
206
207    Enjoy!
208
209    """
210
211
212if __name__ == '__main__':
213    import doctest
214    doctest.testmod()
215