1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 4 """ 5 This module implements a class for handling URLs. 6 """ 7 from six.moves.urllib.parse import parse_qsl, quote, unquote, urlencode 8 import cgi 9 from paste import request 10 import six 11 12 # Imported lazily from FormEncode: 13 variabledecode = None 14 15 __all__ = ["URL", "Image"] 16 17 def html_quote(v): 18 if v is None: 19 return '' 20 return cgi.escape(str(v), 1) 21 22 def url_quote(v): 23 if v is None: 24 return '' 25 return quote(str(v)) 26 27 def js_repr(v): 28 if v is None: 29 return 'null' 30 elif v is False: 31 return 'false' 32 elif v is True: 33 return 'true' 34 elif isinstance(v, list): 35 return '[%s]' % ', '.join(map(js_repr, v)) 36 elif isinstance(v, dict): 37 return '{%s}' % ', '.join( 38 ['%s: %s' % (js_repr(key), js_repr(value)) 39 for key, value in v]) 40 elif isinstance(v, str): 41 return repr(v) 42 elif isinstance(v, unicode): 43 # @@: how do you do Unicode literals in Javascript? 44 return repr(v.encode('UTF-8')) 45 elif isinstance(v, (float, int)): 46 return repr(v) 47 elif isinstance(v, long): 48 return repr(v).lstrip('L') 49 elif hasattr(v, '__js_repr__'): 50 return v.__js_repr__() 51 else: 52 raise ValueError( 53 "I don't know how to turn %r into a Javascript representation" 54 % v) 55 56 class URLResource(object): 57 58 """ 59 This is an abstract superclass for different kinds of URLs 60 """ 61 62 default_params = {} 63 64 def __init__(self, url, vars=None, attrs=None, 65 params=None): 66 self.url = url or '/' 67 self.vars = vars or [] 68 self.attrs = attrs or {} 69 self.params = self.default_params.copy() 70 self.original_params = params or {} 71 if params: 72 self.params.update(params) 73 74 #@classmethod 75 def from_environ(cls, environ, with_query_string=True, 76 with_path_info=True, script_name=None, 77 path_info=None, querystring=None): 78 url = request.construct_url( 79 environ, with_query_string=False, 80 with_path_info=with_path_info, script_name=script_name, 81 path_info=path_info) 82 if with_query_string: 83 if querystring is None: 84 vars = request.parse_querystring(environ) 85 else: 86 vars = parse_qsl( 87 querystring, 88 keep_blank_values=True, 89 strict_parsing=False) 90 else: 91 vars = None 92 v = cls(url, vars=vars) 93 return v 94 95 from_environ = classmethod(from_environ) 96 97 def __call__(self, *args, **kw): 98 res = self._add_positional(args) 99 res = res._add_vars(kw) 100 return res 101 102 def __getitem__(self, item): 103 if '=' in item: 104 name, value = item.split('=', 1) 105 return self._add_vars({unquote(name): unquote(value)}) 106 return self._add_positional((item,)) 107 108 def attr(self, **kw): 109 for key in kw.keys(): 110 if key.endswith('_'): 111 kw[key[:-1]] = kw[key] 112 del kw[key] 113 new_attrs = self.attrs.copy() 114 new_attrs.update(kw) 115 return self.__class__(self.url, vars=self.vars, 116 attrs=new_attrs, 117 params=self.original_params) 118 119 def param(self, **kw): 120 new_params = self.original_params.copy() 121 new_params.update(kw) 122 return self.__class__(self.url, vars=self.vars, 123 attrs=self.attrs, 124 params=new_params) 125 126 def coerce_vars(self, vars): 127 global variabledecode 128 need_variable_encode = False 129 for key, value in vars.items(): 130 if isinstance(value, dict): 131 need_variable_encode = True 132 if key.endswith('_'): 133 vars[key[:-1]] = vars[key] 134 del vars[key] 135 if need_variable_encode: 136 if variabledecode is None: 137 from formencode import variabledecode 138 vars = variabledecode.variable_encode(vars) 139 return vars 140 141 142 def var(self, **kw): 143 kw = self.coerce_vars(kw) 144 new_vars = self.vars + list(kw.items()) 145 return self.__class__(self.url, vars=new_vars, 146 attrs=self.attrs, 147 params=self.original_params) 148 149 def setvar(self, **kw): 150 """ 151 Like ``.var(...)``, except overwrites keys, where .var simply 152 extends the keys. Setting a variable to None here will 153 effectively delete it. 154 """ 155 kw = self.coerce_vars(kw) 156 new_vars = [] 157 for name, values in self.vars: 158 if name in kw: 159 continue 160 new_vars.append((name, values)) 161 new_vars.extend(kw.items()) 162 return self.__class__(self.url, vars=new_vars, 163 attrs=self.attrs, 164 params=self.original_params) 165 166 def setvars(self, **kw): 167 """ 168 Creates a copy of this URL, but with all the variables set/reset 169 (like .setvar(), except clears past variables at the same time) 170 """ 171 return self.__class__(self.url, vars=kw.items(), 172 attrs=self.attrs, 173 params=self.original_params) 174 175 def addpath(self, *paths): 176 u = self 177 for path in paths: 178 path = str(path).lstrip('/') 179 new_url = u.url 180 if not new_url.endswith('/'): 181 new_url += '/' 182 u = u.__class__(new_url+path, vars=u.vars, 183 attrs=u.attrs, 184 params=u.original_params) 185 return u 186 187 if six.PY3: 188 __truediv__ = addpath 189 else: 190 __div__ = addpath 191 192 def become(self, OtherClass): 193 return OtherClass(self.url, vars=self.vars, 194 attrs=self.attrs, 195 params=self.original_params) 196 197 def href__get(self): 198 s = self.url 199 if self.vars: 200 s += '?' 201 vars = [] 202 for name, val in self.vars: 203 if isinstance(val, (list, tuple)): 204 val = [v for v in val if v is not None] 205 elif val is None: 206 continue 207 vars.append((name, val)) 208 s += urlencode(vars, True) 209 return s 210 211 href = property(href__get) 212 213 def __repr__(self): 214 base = '<%s %s' % (self.__class__.__name__, 215 self.href or "''") 216 if self.attrs: 217 base += ' attrs(%s)' % ( 218 ' '.join(['%s="%s"' % (html_quote(n), html_quote(v)) 219 for n, v in self.attrs.items()])) 220 if self.original_params: 221 base += ' params(%s)' % ( 222 ', '.join(['%s=%r' % (n, v) 223 for n, v in self.attrs.items()])) 224 return base + '>' 225 226 def html__get(self): 227 if not self.params.get('tag'): 228 raise ValueError( 229 "You cannot get the HTML of %r until you set the " 230 "'tag' param'" % self) 231 content = self._get_content() 232 tag = '<%s' % self.params.get('tag') 233 attrs = ' '.join([ 234 '%s="%s"' % (html_quote(n), html_quote(v)) 235 for n, v in self._html_attrs()]) 236 if attrs: 237 tag += ' ' + attrs 238 tag += self._html_extra() 239 if content is None: 240 return tag + ' />' 241 else: 242 return '%s>%s</%s>' % (tag, content, self.params.get('tag')) 243 244 html = property(html__get) 245 246 def _html_attrs(self): 247 return self.attrs.items() 248 249 def _html_extra(self): 250 return '' 251 252 def _get_content(self): 253 """ 254 Return the content for a tag (for self.html); return None 255 for an empty tag (like ``<img />``) 256 """ 257 raise NotImplementedError 258 259 def _add_vars(self, vars): 260 raise NotImplementedError 261 262 def _add_positional(self, args): 263 raise NotImplementedError 264 265 class URL(URLResource): 266 267 r""" 268 >>> u = URL('http://localhost') 269 >>> u 270 <URL http://localhost> 271 >>> u = u['view'] 272 >>> str(u) 273 'http://localhost/view' 274 >>> u['//foo'].param(content='view').html 275 '<a href="http://localhost/view/foo">view</a>' 276 >>> u.param(confirm='Really?', content='goto').html 277 '<a href="http://localhost/view" onclick="return confirm(\'Really?\')">goto</a>' 278 >>> u(title='See "it"', content='goto').html 279 '<a href="http://localhost/view?title=See+%22it%22">goto</a>' 280 >>> u('another', var='fuggetaboutit', content='goto').html 281 '<a href="http://localhost/view/another?var=fuggetaboutit">goto</a>' 282 >>> u.attr(content='goto').html 283 Traceback (most recent call last): 284 .... 285 ValueError: You must give a content param to <URL http://localhost/view attrs(content="goto")> generate anchor tags 286 >>> str(u['foo=bar%20stuff']) 287 'http://localhost/view?foo=bar+stuff' 288 """ 289 290 default_params = {'tag': 'a'} 291 292 def __str__(self): 293 return self.href 294 295 def _get_content(self): 296 if not self.params.get('content'): 297 raise ValueError( 298 "You must give a content param to %r generate anchor tags" 299 % self) 300 return self.params['content'] 301 302 def _add_vars(self, vars): 303 url = self 304 for name in ('confirm', 'content'): 305 if name in vars: 306 url = url.param(**{name: vars.pop(name)}) 307 if 'target' in vars: 308 url = url.attr(target=vars.pop('target')) 309 return url.var(**vars) 310 311 def _add_positional(self, args): 312 return self.addpath(*args) 313 314 def _html_attrs(self): 315 attrs = list(self.attrs.items()) 316 attrs.insert(0, ('href', self.href)) 317 if self.params.get('confirm'): 318 attrs.append(('onclick', 'return confirm(%s)' 319 % js_repr(self.params['confirm']))) 320 return attrs 321 322 def onclick_goto__get(self): 323 return 'location.href=%s; return false' % js_repr(self.href) 324 325 onclick_goto = property(onclick_goto__get) 326 327 def button__get(self): 328 return self.become(Button) 329 330 button = property(button__get) 331 332 def js_popup__get(self): 333 return self.become(JSPopup) 334 335 js_popup = property(js_popup__get) 336 337 class Image(URLResource): 338 339 r""" 340 >>> i = Image('/images') 341 >>> i = i / '/foo.png' 342 >>> i.html 343 '<img src="/images/foo.png" />' 344 >>> str(i['alt=foo']) 345 '<img src="/images/foo.png" alt="foo" />' 346 >>> i.href 347 '/images/foo.png' 348 """ 349 350 default_params = {'tag': 'img'} 351 352 def __str__(self): 353 return self.html 354 355 def _get_content(self): 356 return None 357 358 def _add_vars(self, vars): 359 return self.attr(**vars) 360 361 def _add_positional(self, args): 362 return self.addpath(*args) 363 364 def _html_attrs(self): 365 attrs = list(self.attrs.items()) 366 attrs.insert(0, ('src', self.href)) 367 return attrs 368 369 class Button(URLResource): 370 371 r""" 372 >>> u = URL('/') 373 >>> u = u / 'delete' 374 >>> b = u.button['confirm=Sure?'](id=5, content='del') 375 >>> str(b) 376 '<button onclick="if (confirm(\'Sure?\')) {location.href=\'/delete?id=5\'}; return false">del</button>' 377 """ 378 379 default_params = {'tag': 'button'} 380 381 def __str__(self): 382 return self.html 383 384 def _get_content(self): 385 if self.params.get('content'): 386 return self.params['content'] 387 if self.attrs.get('value'): 388 return self.attrs['content'] 389 # @@: Error? 390 return None 391 392 def _add_vars(self, vars): 393 button = self 394 if 'confirm' in vars: 395 button = button.param(confirm=vars.pop('confirm')) 396 if 'content' in vars: 397 button = button.param(content=vars.pop('content')) 398 return button.var(**vars) 399 400 def _add_positional(self, args): 401 return self.addpath(*args) 402 403 def _html_attrs(self): 404 attrs = list(self.attrs.items()) 405 onclick = 'location.href=%s' % js_repr(self.href) 406 if self.params.get('confirm'): 407 onclick = 'if (confirm(%s)) {%s}' % ( 408 js_repr(self.params['confirm']), onclick) 409 onclick += '; return false' 410 attrs.insert(0, ('onclick', onclick)) 411 return attrs 412 413 class JSPopup(URLResource): 414 415 r""" 416 >>> u = URL('/') 417 >>> u = u / 'view' 418 >>> j = u.js_popup(content='view') 419 >>> j.html 420 '<a href="/view" onclick="window.open(\'/view\', \'_blank\'); return false" target="_blank">view</a>' 421 """ 422 423 default_params = {'tag': 'a', 'target': '_blank'} 424 425 def _add_vars(self, vars): 426 button = self 427 for var in ('width', 'height', 'stripped', 'content'): 428 if var in vars: 429 button = button.param(**{var: vars.pop(var)}) 430 return button.var(**vars) 431 432 def _window_args(self): 433 p = self.params 434 features = [] 435 if p.get('stripped'): 436 p['location'] = p['status'] = p['toolbar'] = '0' 437 for param in 'channelmode directories fullscreen location menubar resizable scrollbars status titlebar'.split(): 438 if param not in p: 439 continue 440 v = p[param] 441 if v not in ('yes', 'no', '1', '0'): 442 if v: 443 v = '1' 444 else: 445 v = '0' 446 features.append('%s=%s' % (param, v)) 447 for param in 'height left top width': 448 if not p.get(param): 449 continue 450 features.append('%s=%s' % (param, p[param])) 451 args = [self.href, p['target']] 452 if features: 453 args.append(','.join(features)) 454 return ', '.join(map(js_repr, args)) 455 456 def _html_attrs(self): 457 attrs = list(self.attrs.items()) 458 onclick = ('window.open(%s); return false' 459 % self._window_args()) 460 attrs.insert(0, ('target', self.params['target'])) 461 attrs.insert(0, ('onclick', onclick)) 462 attrs.insert(0, ('href', self.href)) 463 return attrs 464 465 def _get_content(self): 466 if not self.params.get('content'): 467 raise ValueError( 468 "You must give a content param to %r generate anchor tags" 469 % self) 470 return self.params['content'] 471 472 def _add_positional(self, args): 473 return self.addpath(*args) 474 475 if __name__ == '__main__': 476 import doctest 477 doctest.testmod() 478 479