Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from __future__ import absolute_import 

2 

3import re 

4import logging 

5import itertools 

6import os 

7import webob as wo 

8import pkg_resources as pr 

9import mimetypes 

10import inspect 

11import warnings 

12import wsgiref.util 

13 

14from .widgets import Widget 

15from .util import MultipleReplacer 

16import tw2.core.core 

17from .params import Param, Variable, ParameterError, Required 

18from .middleware import register_resource 

19from .js import encoder, js_symbol 

20 

21from markupsafe import Markup 

22import six 

23 

24log = logging.getLogger(__name__) 

25 

26 

27# TBD is there a better place to put this? 

28mimetypes.init() 

29mimetypes.types_map['.ico'] = 'image/x-icon' 

30 

31 

32class JSSymbol(js_symbol): 

33 """ Deprecated compatibility shim with old TW2 stuff. Use js_symbol. """ 

34 

35 def __init__(self, *args, **kw): 

36 warnings.warn("JSSymbol is deprecated. Please use js_symbol") 

37 

38 if len(args) > 1: 

39 raise ValueError("JSSymbol must receive up to only one arg.") 

40 

41 if len(args) == 1 and 'src' in kw: 

42 raise ValueError("JSSymbol must receive only one src arg.") 

43 

44 if len(args) == 1: 

45 kw['src'] = args[0] 

46 

47 super(JSSymbol, self).__init__(**kw) 

48 

49 # Backwards compatibility for accessing the source. 

50 self.src = self._name 

51 

52 

53class ResourceBundle(Widget): 

54 """ Just a list of resources. 

55 

56 Use it as follows: 

57 

58 >>> jquery_ui = ResourceBundle(resources=[jquery_js, jquery_css]) 

59 >>> jquery_ui.inject() 

60 

61 """ 

62 

63 @classmethod 

64 def inject(cls): 

65 cls.req().prepare() 

66 

67 def prepare(self): 

68 super(ResourceBundle, self).prepare() 

69 

70 rl = tw2.core.core.request_local() 

71 rl_resources = rl.setdefault('resources', []) 

72 rl_location = rl['middleware'].config.inject_resources_location 

73 

74 if self not in rl_resources: 

75 for r in self.resources: 

76 r.req().prepare() 

77 

78 

79class Resource(ResourceBundle): 

80 """A resource required by a widget being displayed. 

81 

82 ``location`` states where the resource should be injected 

83 into the page. Can be any of ``head``, ``headbottom``, 

84 ``bodytop`` or ``bodybottom`` or ``None``. 

85 """ 

86 location = Param( 

87 'Location on the page where the resource should be placed.' \ 

88 'This can be one of: head, headbottom, bodytop or bodybottom. '\ 

89 'None means the resource will not be injected, which is still '\ 

90 'useful, e.g. static images.', default=None) 

91 id = None 

92 template = None 

93 

94 def prepare(self): 

95 super(Resource, self).prepare() 

96 

97 rl = tw2.core.core.request_local() 

98 rl_resources = rl.setdefault('resources', []) 

99 rl_location = rl['middleware'].config.inject_resources_location 

100 

101 if self not in rl_resources: 

102 if self.location is '__use_middleware': 

103 self.location = rl_location 

104 

105 rl_resources.append(self) 

106 

107 

108class Link(Resource): 

109 ''' 

110 A link to a file. 

111 

112 The ``link`` parameter can be used to specify the explicit 

113 link to a URL. 

114 

115 If omitted, the link will be built to serve ``filename`` 

116 from ``modname`` as a resource coming from a python 

117 distribution. 

118 ''' 

119 id = None 

120 link = Param( 

121 'Direct web link to file. If this is not specified, it is ' + 

122 'automatically generated, based on :attr:`modname` and ' + 

123 ':attr:`filename`.', 

124 ) 

125 modname = Param( 

126 'Name of Python module that contains the file.', 

127 default=None, 

128 ) 

129 filename = Param( 

130 'Path to file, relative to module base.', 

131 default=None, 

132 ) 

133 no_inject = Param( 

134 "Don't inject this link. (Default: False)", 

135 default=False, 

136 ) 

137 whole_dir = Param( 

138 "Make the whole directory available. (Default: False)", 

139 default=False, 

140 ) 

141 

142 @classmethod 

143 def guess_modname(cls): 

144 """ Try to guess my modname. 

145 

146 If I wasn't supplied any modname, take a guess by stepping back up the 

147 frame stack until I find something not in tw2.core 

148 """ 

149 

150 try: 

151 frame, i = inspect.stack()[0][0], 0 

152 while frame.f_globals['__name__'].startswith('tw2.core'): 

153 frame, i = inspect.stack()[i][0], i + 1 

154 

155 return frame.f_globals['__name__'] 

156 except Exception: 

157 return None 

158 

159 @classmethod 

160 def post_define(cls): 

161 

162 if not cls.no_inject: 

163 if getattr(cls, 'filename', None) and \ 

164 type(cls.filename) != property: 

165 

166 if not cls.modname: 

167 cls.modname = cls.guess_modname() 

168 

169 register_resource( 

170 cls.modname or '__anon__', cls.filename, cls.whole_dir 

171 ) 

172 

173 def prepare(self): 

174 rl = tw2.core.core.request_local() 

175 if not self.no_inject: 

176 if not hasattr(self, 'link'): 

177 # TBD shouldn't we test for this in __new__ ? 

178 if not self.filename: 

179 raise ParameterError( 

180 "Either 'link' or 'filename' must be specified" 

181 ) 

182 resources = rl['middleware'].resources 

183 self.link = resources.resource_path( 

184 self.modname or '__anon__', self.filename 

185 ) 

186 super(Link, self).prepare() 

187 

188 def __hash__(self): 

189 return hash( 

190 hasattr(self, 'link') and \ 

191 self.link or \ 

192 ((self.modname or '') + self.filename) 

193 ) 

194 

195 def __eq__(self, other): 

196 return (isinstance(other, Link) and self.link == other.link 

197 and self.modname == other.modname 

198 and self.filename == other.filename) 

199 

200 def __repr__(self): 

201 return "%s('%s')" % ( 

202 self.__class__.__name__, 

203 getattr(self, 'link', '%s/%s' % (self.modname, self.filename)) 

204 ) 

205 

206 

207class DirLink(Link): 

208 ''' A whole directory as a resource. 

209 

210 Unlike :class:`JSLink` and :class:`CSSLink`, this resource doesn't inject 

211 anything on the page.. but it does register all resources under the 

212 marked directory to be served by the middleware. 

213 

214 This is useful if you have a css file that pulls in a number of other 

215 static resources like icons and images. 

216 ''' 

217 link = Variable() 

218 filename = Required 

219 whole_dir = True 

220 

221 def prepare(self): 

222 resources = tw2.core.core.request_local()['middleware'].resources 

223 self.link = resources.resource_path( 

224 self.modname, 

225 self.filename, 

226 ) 

227 

228 

229class JSLink(Link): 

230 ''' 

231 A JavaScript source file. 

232 

233 By default is injected in whatever default place 

234 is specified by the middleware. 

235 ''' 

236 location = '__use_middleware' 

237 template = 'tw2.core.templates.jslink' 

238 

239 

240class CSSLink(Link): 

241 ''' 

242 A CSS style sheet. 

243 

244 By default it's injected at the top of the head node. 

245 ''' 

246 media = Param('Media tag', default='all') 

247 location = 'head' 

248 template = 'tw2.core.templates.csslink' 

249 

250 

251class JSSource(Resource): 

252 """ 

253 Inline JavaScript source code. 

254 

255 By default is injected before the </body> is closed 

256 """ 

257 src = Param('Source code', default=None) 

258 location = 'bodybottom' 

259 template = 'tw2.core.templates.jssource' 

260 

261 def __eq__(self, other): 

262 return isinstance(other, JSSource) and self.src == other.src 

263 

264 def __repr__(self): 

265 return "%s('%s')" % (self.__class__.__name__, self.src) 

266 

267 def prepare(self): 

268 super(JSSource, self).prepare() 

269 if not self.src: 

270 raise ValueError("%r must be provided a 'src' attr" % self) 

271 self.src = Markup(self.src) 

272 

273 

274class CSSSource(Resource): 

275 """ 

276 Inline Cascading Style-Sheet code. 

277 

278 By default it's injected at the top of the head node. 

279 """ 

280 src = Param('CSS code', default=None) 

281 location = 'head' 

282 template = 'tw2.core.templates.csssource' 

283 

284 def __eq__(self, other): 

285 return isinstance(other, CSSSource) and self.src == other.src 

286 

287 def __repr__(self): 

288 return "%s('%s')" % (self.__class__.__name__, self.src) 

289 

290 def prepare(self): 

291 super(CSSSource, self).prepare() 

292 if not self.src: 

293 raise ValueError("%r must be provided a 'src' attr" % self) 

294 self.src = Markup(self.src) 

295 

296 

297class _JSFuncCall(JSSource): 

298 """ 

299 Internal use inline JavaScript function call. 

300 

301 Please use tw2.core.js_function(...) externally. 

302 """ 

303 src = None 

304 function = Param('Function name', default=None) 

305 args = Param('Function arguments', default=None) 

306 location = 'bodybottom' # TBD: afterwidget? 

307 

308 def __str__(self): 

309 if not self.src: 

310 self.prepare() 

311 return self.src 

312 

313 def prepare(self): 

314 if not self.src: 

315 args = '' 

316 if isinstance(self.args, dict): 

317 args = encoder.encode(self.args) 

318 elif self.args: 

319 args = ', '.join(encoder.encode(a) for a in self.args) 

320 

321 self.src = '%s(%s)' % (self.function, args) 

322 super(_JSFuncCall, self).prepare() 

323 

324 def __hash__(self): 

325 if self.args: 

326 if isinstance(self.args, dict): 

327 sargs = encoder.encode(self.args) 

328 else: 

329 sargs = ', '.join(encoder.encode(a) for a in self.args) 

330 else: 

331 sargs = None 

332 

333 return hash((hasattr(self, 'src') and self.src or '') + (sargs or '')) 

334 

335 def __eq__(self, other): 

336 return (getattr(self, 'src', None) == getattr(other, 'src', None) 

337 and getattr(self, 'args', None) == getattr(other, 'args', None) 

338 ) 

339 

340 

341class ResourcesApp(object): 

342 """WSGI Middleware to serve static resources 

343 

344 This handles URLs like this: 

345 /resources/tw2.forms/static/forms.css 

346 

347 Where: 

348 resources is the prefix 

349 tw2.forms is a python package name 

350 static is a directory inside the package 

351 forms.css is the file to retrieve 

352 

353 For this to work, the file must have been registered in advance, 

354 using :meth:`register`. There is a ResourcesApp instance for each 

355 TwMiddleware instance. 

356 """ 

357 

358 def __init__(self, config): 

359 self._paths = {} 

360 self._dirs = [] 

361 self.config = config 

362 

363 def register(self, modname, filename, whole_dir=False): 

364 """ Register a file for static serving. 

365 

366 After this method has been called, for say ('tw2.forms', 

367 'static/forms.css'), the URL /resources/tw2.forms/static/forms.css will 

368 then serve that file from within the tw2.forms package. This works 

369 correctly for zipped eggs. 

370 

371 *Security Consideration* - This file will be readable by users of the 

372 application, so make sure it contains no confidential data. For 

373 DirLink resources, the whole directory, and subdirectories will be 

374 readable. 

375 

376 `modname` 

377 The python module that contains the file to publish. You can also 

378 pass a pkg_resources.Requirement instance to point to the root of 

379 an egg distribution. 

380 

381 `filename` 

382 The path, relative to the base of the module, of the file to be 

383 published. If *modname* is None, it's an absolute path. 

384 """ 

385 if isinstance(modname, pr.Requirement): 

386 modname = os.path.basename(pr.working_set.find(modname).location) 

387 

388 path = modname + '/' + filename.lstrip('/') 

389 

390 if whole_dir: 

391 if path not in self._dirs: 

392 self._dirs.append(path) 

393 else: 

394 if path not in self._paths: 

395 self._paths[path] = (modname, filename) 

396 

397 def resource_path(self, modname, filename): 

398 """ Return a resource's web path. """ 

399 

400 if isinstance(modname, pr.Requirement): 

401 modname = os.path.basename(pr.working_set.find(modname).location) 

402 

403 path = modname + '/' + filename.lstrip('/') 

404 return self.config.script_name + self.config.res_prefix + path 

405 

406 def __call__(self, environ, start_response): 

407 req = wo.Request(environ) 

408 try: 

409 path = environ['PATH_INFO'] 

410 path = path[len(self.config.res_prefix):] 

411 

412 if path not in self._paths: 

413 if '..' in path: # protect against directory traversal 

414 raise IOError() 

415 for d in self._dirs: 

416 if path.startswith(d.replace('\\', '/')): 

417 break 

418 else: 

419 raise IOError() 

420 modname, filename = path.lstrip('/').split('/', 1) 

421 ct, enc = mimetypes.guess_type(os.path.basename(filename)) 

422 if modname and modname != '__anon__': 

423 stream = pr.resource_stream(modname, filename) 

424 else: 

425 stream = open(filename) 

426 except IOError: 

427 resp = wo.Response(status="404 Not Found") 

428 else: 

429 stream = wsgiref.util.FileWrapper(stream, self.config.bufsize) 

430 resp = wo.Response(app_iter=stream, content_type=ct) 

431 if enc: 

432 resp.content_type_params['charset'] = enc 

433 resp.cache_control = {'max-age': int(self.config.res_max_age)} 

434 return resp(environ, start_response) 

435 

436 

437class _ResourceInjector(MultipleReplacer): 

438 """ 

439 ToscaWidgets can inject resources that have been registered for injection 

440 in the current request. 

441 

442 Usually widgets register them when they're displayed and they have 

443 instances of :class:`tw2.core.resources.Resource` declared at their 

444 :attr:`tw2.core.Widget.javascript` or :attr:`tw2.core.Widget.css` 

445 attributes. 

446 

447 Resources can also be registered manually from a controller or template by 

448 calling their :meth:`tw2.core.resources.Resource.inject` method. 

449 

450 When a page including widgets is rendered, Resources that are registered 

451 for injection are collected in a request-local storage area (this means 

452 any thing stored here is only visible to one single thread of execution 

453 and that its contents are freed when the request is finished) where they 

454 can be rendered and injected in the resulting html. 

455 

456 ToscaWidgets' middleware can take care of injecting them automatically 

457 (default) but they can also be injected explicitly, example:: 

458 

459 >>> from tw2.core.resources import JSLink, inject_resources 

460 >>> JSLink(link="http://example.com").inject() 

461 >>> html = "<html><head></head><body></body></html>" 

462 >>> inject_resources(html) 

463 '<html><head><script type="text/javascript" 

464 src="http://example.com"></script></head><body></body></html>' 

465 

466 Once resources have been injected they are popped from request local and 

467 cannot be injected again (in the same request). This is useful in case 

468 :class:`injector_middleware` is stacked so it doesn't inject them again. 

469 

470 Injecting them explicitly is necessary if the response's body is being 

471 cached before the middleware has a chance to inject them because when the 

472 cached version is served no widgets are being rendered so they will not 

473 have a chance to register their resources. 

474 """ 

475 

476 def __init__(self): 

477 return MultipleReplacer.__init__(self, { 

478 r'<head(?!er).*?>': self._injector_for_location('head'), 

479 r'</head(?!er).*?>': self._injector_for_location( 

480 'headbottom', False 

481 ), 

482 r'<body.*?>': self._injector_for_location('bodytop'), 

483 r'</body.*?>': self._injector_for_location('bodybottom', False) 

484 }, re.I | re.M) 

485 

486 def _injector_for_location(self, key, after=True): 

487 def inject(group, resources, encoding): 

488 inj = six.u('\n').join([ 

489 r.display(displays_on='string') 

490 for r in resources 

491 if r.location == key 

492 ]) 

493 if after: 

494 return group + inj 

495 return inj + group 

496 return inject 

497 

498 def __call__(self, html, resources=None, encoding=None): 

499 """Injects resources, if any, into html string when called. 

500 

501 .. note:: 

502 Ignore the ``self`` parameter if seeing this as 

503 :func:`tw.core.resource_injector.inject_resources` docstring 

504 since it is an alias for an instance method of a private class. 

505 

506 ``html`` must be a ``encoding`` encoded string. If ``encoding`` is not 

507 given it will be tried to be derived from a <meta>. 

508 

509 """ 

510 if resources is None: 

511 resources = tw2.core.core.request_local().get('resources', None) 

512 if resources: 

513 encoding = encoding or find_charset(html) or 'utf-8' 

514 html = MultipleReplacer.__call__( 

515 self, html, resources, encoding 

516 ) 

517 tw2.core.core.request_local().pop('resources', None) 

518 return html 

519 

520 

521# Bind __call__ directly so docstring is included in docs 

522inject_resources = _ResourceInjector().__call__ 

523 

524 

525_charset_re = re.compile( 

526 r"charset\s*=\s*(?P<charset>[\w-]+)([^\>])*", re.I | re.M) 

527 

528 

529def find_charset(string): 

530 m = _charset_re.search(string) 

531 if m: 

532 return m.group('charset').lower()