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

1# -*- coding: utf-8 -*- 

2import json 

3import os 

4 

5from os.path import getmtime, normcase, normpath, join, isdir, exists 

6 

7from pkg_resources import resource_exists, resource_filename, resource_isdir 

8 

9from pyramid.asset import abspath_from_asset_spec, resolve_asset_spec 

10 

11from pyramid.compat import lru_cache, text_ 

12 

13from pyramid.httpexceptions import HTTPNotFound, HTTPMovedPermanently 

14 

15from pyramid.path import caller_package 

16 

17from pyramid.response import _guess_type, FileResponse 

18 

19from pyramid.traversal import traversal_path_info 

20 

21slash = text_('/') 

22 

23 

24class static_view(object): 

25 """ An instance of this class is a callable which can act as a 

26 :app:`Pyramid` :term:`view callable`; this view will serve 

27 static files from a directory on disk based on the ``root_dir`` 

28 you provide to its constructor. 

29 

30 The directory may contain subdirectories (recursively); the static 

31 view implementation will descend into these directories as 

32 necessary based on the components of the URL in order to resolve a 

33 path into a response. 

34 

35 You may pass an absolute or relative filesystem path or a 

36 :term:`asset specification` representing the directory 

37 containing static files as the ``root_dir`` argument to this 

38 class' constructor. 

39 

40 If the ``root_dir`` path is relative, and the ``package_name`` 

41 argument is ``None``, ``root_dir`` will be considered relative to 

42 the directory in which the Python file which *calls* ``static`` 

43 resides. If the ``package_name`` name argument is provided, and a 

44 relative ``root_dir`` is provided, the ``root_dir`` will be 

45 considered relative to the Python :term:`package` specified by 

46 ``package_name`` (a dotted path to a Python package). 

47 

48 ``cache_max_age`` influences the ``Expires`` and ``Max-Age`` 

49 response headers returned by the view (default is 3600 seconds or 

50 one hour). 

51 

52 ``use_subpath`` influences whether ``request.subpath`` will be used as 

53 ``PATH_INFO`` when calling the underlying WSGI application which actually 

54 serves the static files. If it is ``True``, the static application will 

55 consider ``request.subpath`` as ``PATH_INFO`` input. If it is ``False``, 

56 the static application will consider request.environ[``PATH_INFO``] as 

57 ``PATH_INFO`` input. By default, this is ``False``. 

58 

59 .. note:: 

60 

61 If the ``root_dir`` is relative to a :term:`package`, or is a 

62 :term:`asset specification` the :app:`Pyramid` 

63 :class:`pyramid.config.Configurator` method can be used to override 

64 assets within the named ``root_dir`` package-relative directory. 

65 However, if the ``root_dir`` is absolute, configuration will not be able 

66 to override the assets it contains. 

67 """ 

68 

69 def __init__( 

70 self, 

71 root_dir, 

72 cache_max_age=3600, 

73 package_name=None, 

74 use_subpath=False, 

75 index='index.html', 

76 ): 

77 # package_name is for bw compat; it is preferred to pass in a 

78 # package-relative path as root_dir 

79 # (e.g. ``anotherpackage:foo/static``). 

80 self.cache_max_age = cache_max_age 

81 if package_name is None: 

82 package_name = caller_package().__name__ 

83 package_name, docroot = resolve_asset_spec(root_dir, package_name) 

84 self.use_subpath = use_subpath 

85 self.package_name = package_name 

86 self.docroot = docroot 

87 self.norm_docroot = normcase(normpath(docroot)) 

88 self.index = index 

89 

90 def __call__(self, context, request): 

91 if self.use_subpath: 

92 path_tuple = request.subpath 

93 else: 

94 path_tuple = traversal_path_info(request.environ['PATH_INFO']) 

95 path = _secure_path(path_tuple) 

96 

97 if path is None: 

98 raise HTTPNotFound('Out of bounds: %s' % request.url) 

99 

100 if self.package_name: # package resource 

101 resource_path = '%s/%s' % (self.docroot.rstrip('/'), path) 

102 if resource_isdir(self.package_name, resource_path): 

103 if not request.path_url.endswith('/'): 

104 self.add_slash_redirect(request) 

105 resource_path = '%s/%s' % ( 

106 resource_path.rstrip('/'), 

107 self.index, 

108 ) 

109 

110 if not resource_exists(self.package_name, resource_path): 

111 raise HTTPNotFound(request.url) 

112 filepath = resource_filename(self.package_name, resource_path) 

113 

114 else: # filesystem file 

115 

116 # os.path.normpath converts / to \ on windows 

117 filepath = normcase(normpath(join(self.norm_docroot, path))) 

118 if isdir(filepath): 

119 if not request.path_url.endswith('/'): 

120 self.add_slash_redirect(request) 

121 filepath = join(filepath, self.index) 

122 if not exists(filepath): 

123 raise HTTPNotFound(request.url) 

124 

125 content_type, content_encoding = _guess_type(filepath) 

126 return FileResponse( 

127 filepath, 

128 request, 

129 self.cache_max_age, 

130 content_type, 

131 content_encoding=None, 

132 ) 

133 

134 def add_slash_redirect(self, request): 

135 url = request.path_url + '/' 

136 qs = request.query_string 

137 if qs: 

138 url = url + '?' + qs 

139 raise HTTPMovedPermanently(url) 

140 

141 

142_seps = set(['/', os.sep]) 

143 

144 

145def _contains_slash(item): 

146 for sep in _seps: 

147 if sep in item: 

148 return True 

149 

150 

151_has_insecure_pathelement = set(['..', '.', '']).intersection 

152 

153 

154@lru_cache(1000) 

155def _secure_path(path_tuple): 

156 if _has_insecure_pathelement(path_tuple): 

157 # belt-and-suspenders security; this should never be true 

158 # unless someone screws up the traversal_path code 

159 # (request.subpath is computed via traversal_path too) 

160 return None 

161 if any([_contains_slash(item) for item in path_tuple]): 

162 return None 

163 encoded = slash.join(path_tuple) # will be unicode 

164 return encoded 

165 

166 

167class QueryStringCacheBuster(object): 

168 """ 

169 An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds 

170 a token for cache busting in the query string of an asset URL. 

171 

172 The optional ``param`` argument determines the name of the parameter added 

173 to the query string and defaults to ``'x'``. 

174 

175 To use this class, subclass it and provide a ``tokenize`` method which 

176 accepts ``request, pathspec, kw`` and returns a token. 

177 

178 .. versionadded:: 1.6 

179 """ 

180 

181 def __init__(self, param='x'): 

182 self.param = param 

183 

184 def __call__(self, request, subpath, kw): 

185 token = self.tokenize(request, subpath, kw) 

186 query = kw.setdefault('_query', {}) 

187 if isinstance(query, dict): 

188 query[self.param] = token 

189 else: 

190 kw['_query'] = tuple(query) + ((self.param, token),) 

191 return subpath, kw 

192 

193 

194class QueryStringConstantCacheBuster(QueryStringCacheBuster): 

195 """ 

196 An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds 

197 an arbitrary token for cache busting in the query string of an asset URL. 

198 

199 The ``token`` parameter is the token string to use for cache busting and 

200 will be the same for every request. 

201 

202 The optional ``param`` argument determines the name of the parameter added 

203 to the query string and defaults to ``'x'``. 

204 

205 .. versionadded:: 1.6 

206 """ 

207 

208 def __init__(self, token, param='x'): 

209 super(QueryStringConstantCacheBuster, self).__init__(param=param) 

210 self._token = token 

211 

212 def tokenize(self, request, subpath, kw): 

213 return self._token 

214 

215 

216class ManifestCacheBuster(object): 

217 """ 

218 An implementation of :class:`~pyramid.interfaces.ICacheBuster` which 

219 uses a supplied manifest file to map an asset path to a cache-busted 

220 version of the path. 

221 

222 The ``manifest_spec`` can be an absolute path or a :term:`asset 

223 specification` pointing to a package-relative file. 

224 

225 The manifest file is expected to conform to the following simple JSON 

226 format: 

227 

228 .. code-block:: json 

229 

230 { 

231 "css/main.css": "css/main-678b7c80.css", 

232 "images/background.png": "images/background-a8169106.png", 

233 } 

234 

235 By default, it is a JSON-serialized dictionary where the keys are the 

236 source asset paths used in calls to 

237 :meth:`~pyramid.request.Request.static_url`. For example: 

238 

239 .. code-block:: pycon 

240 

241 >>> request.static_url('myapp:static/css/main.css') 

242 "http://www.example.com/static/css/main-678b7c80.css" 

243 

244 The file format and location can be changed by subclassing and overriding 

245 :meth:`.parse_manifest`. 

246 

247 If a path is not found in the manifest it will pass through unchanged. 

248 

249 If ``reload`` is ``True`` then the manifest file will be reloaded when 

250 changed. It is not recommended to leave this enabled in production. 

251 

252 If the manifest file cannot be found on disk it will be treated as 

253 an empty mapping unless ``reload`` is ``False``. 

254 

255 .. versionadded:: 1.6 

256 """ 

257 

258 exists = staticmethod(exists) # testing 

259 getmtime = staticmethod(getmtime) # testing 

260 

261 def __init__(self, manifest_spec, reload=False): 

262 package_name = caller_package().__name__ 

263 self.manifest_path = abspath_from_asset_spec( 

264 manifest_spec, package_name 

265 ) 

266 self.reload = reload 

267 

268 self._mtime = None 

269 if not reload: 

270 self._manifest = self.get_manifest() 

271 

272 def get_manifest(self): 

273 with open(self.manifest_path, 'rb') as fp: 

274 return self.parse_manifest(fp.read()) 

275 

276 def parse_manifest(self, content): 

277 """ 

278 Parse the ``content`` read from the ``manifest_path`` into a 

279 dictionary mapping. 

280 

281 Subclasses may override this method to use something other than 

282 ``json.loads`` to load any type of file format and return a conforming 

283 dictionary. 

284 

285 """ 

286 return json.loads(content.decode('utf-8')) 

287 

288 @property 

289 def manifest(self): 

290 """ The current manifest dictionary.""" 

291 if self.reload: 

292 if not self.exists(self.manifest_path): 

293 return {} 

294 mtime = self.getmtime(self.manifest_path) 

295 if self._mtime is None or mtime > self._mtime: 

296 self._manifest = self.get_manifest() 

297 self._mtime = mtime 

298 return self._manifest 

299 

300 def __call__(self, request, subpath, kw): 

301 subpath = self.manifest.get(subpath, subpath) 

302 return (subpath, kw)