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