Coverage for gws-app/gws/base/web/action.py: 0%

134 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""Handle dynamic assets. 

2 

3An asset is a file located in a global or project-specific ``assets`` directory. 

4 

5In order to access a project asset, the user must have ``read`` permission for the project itself. 

6 

7When the Web application receives a ``webAsset`` request with a ``path`` argument, it first checks the project-specific assets directory, 

8and then the global dir. 

9 

10If the file is found, and its name matches :obj:`gws.base.template.manager.TEMPLATE_TYPES`, a respective ``Template`` object is generated on the fly and rendered. 

11The renderer is passed a :obj:`TemplateArgs` object as an argument. 

12The :obj:`gws.Response` object returned from rendering is passed back to the user. 

13 

14If the file is not a template and matches the ``allowMime/denyMime`` filter, its content is returned to the user. 

15""" 

16 

17from typing import Optional, cast 

18 

19import os 

20import re 

21 

22import gws 

23import gws.base.action 

24import gws.base.client.bundles 

25import gws.base.template 

26import gws.lib.mime 

27import gws.lib.osx 

28import gws.lib.intl 

29 

30gws.ext.new.action('web') 

31 

32 

33class TemplateArgs(gws.TemplateArgs): 

34 """Asset template arguments.""" 

35 

36 project: Optional[gws.Project] 

37 """Current project.""" 

38 projects: list[gws.Project] 

39 """List of user projects.""" 

40 req: gws.WebRequester 

41 """Requester object.""" 

42 user: gws.User 

43 """Current user.""" 

44 params: dict 

45 """Request parameters.""" 

46 locale: gws.Locale 

47 """Locale object.""" 

48 

49 

50class Config(gws.base.action.Config): 

51 pass 

52 

53 

54class Props(gws.base.action.Props): 

55 pass 

56 

57 

58class AssetRequest(gws.Request): 

59 path: str 

60 

61 

62class AssetResponse(gws.Request): 

63 content: str 

64 mime: str 

65 

66 

67class FileRequest(gws.Request): 

68 preview: bool = False 

69 modelUid: str 

70 fieldName: str 

71 featureUid: str 

72 

73 

74class Object(gws.base.action.Object): 

75 """Web action""" 

76 

77 @gws.ext.command.api('webAsset') 

78 def api_asset(self, req: gws.WebRequester, p: AssetRequest) -> AssetResponse: 

79 """Return an asset under the given path and project""" 

80 r = self._serve_path(req, p) 

81 if r.contentPath: 

82 r.content = gws.u.read_file_b(r.contentPath) 

83 return AssetResponse(content=r.content, mime=r.mime) 

84 

85 @gws.ext.command.get('webAsset') 

86 def http_asset(self, req: gws.WebRequester, p: AssetRequest) -> gws.ContentResponse: 

87 r = self._serve_path(req, p) 

88 return r 

89 

90 @gws.ext.command.get('webDownload') 

91 def download(self, req: gws.WebRequester, p) -> gws.ContentResponse: 

92 r = self._serve_path(req, p) 

93 r.asAttachment = True 

94 return r 

95 

96 @gws.ext.command.get('webFile') 

97 def file(self, req: gws.WebRequester, p: FileRequest) -> gws.ContentResponse: 

98 model = cast(gws.Model, req.user.acquire(p.modelUid, gws.ext.object.model, gws.Access.read)) 

99 field = model.field(p.fieldName) 

100 if not field: 

101 raise gws.NotFoundError() 

102 fn = getattr(field, 'handle_web_file_request', None) 

103 if not fn: 

104 raise gws.NotFoundError() 

105 mc = gws.ModelContext( 

106 op=gws.ModelOperation.read, 

107 user=req.user, 

108 project=req.user.require_project(p.projectUid), 

109 maxDepth=0, 

110 ) 

111 res = fn(p.featureUid, p.preview, mc) 

112 if not res: 

113 raise gws.NotFoundError() 

114 return res 

115 

116 @gws.ext.command.get('webSystemAsset') 

117 def sys_asset(self, req: gws.WebRequester, p: AssetRequest) -> gws.ContentResponse: 

118 locale = gws.lib.intl.locale(p.localeUid, self.root.app.localeUids) 

119 

120 # only accept '8.0.0.vendor.js' etc or simply 'vendor.js' 

121 path = p.path 

122 if path.startswith(self.root.app.version): 

123 path = path[len(self.root.app.version) + 1:] 

124 

125 if path == 'vendor.js': 

126 return gws.ContentResponse( 

127 mime=gws.lib.mime.JS, 

128 content=gws.base.client.bundles.javascript(self.root, 'vendor', locale)) 

129 

130 if path == 'util.js': 

131 return gws.ContentResponse( 

132 mime=gws.lib.mime.JS, 

133 content=gws.base.client.bundles.javascript(self.root, 'util', locale)) 

134 

135 if path == 'app.js': 

136 return gws.ContentResponse( 

137 mime=gws.lib.mime.JS, 

138 content=gws.base.client.bundles.javascript(self.root, 'app', locale)) 

139 

140 if path.endswith('.css'): 

141 s = path.split('.') 

142 if len(s) != 2: 

143 raise gws.NotFoundError(f'invalid css request: {p.path=}') 

144 content = gws.base.client.bundles.css(self.root, 'app', s[0]) 

145 if not content: 

146 raise gws.NotFoundError(f'invalid css request: {p.path=}') 

147 return gws.ContentResponse(mime=gws.lib.mime.CSS, content=content) 

148 

149 raise gws.NotFoundError(f'invalid system asset: {p.path=}') 

150 

151 def _serve_path(self, req: gws.WebRequester, p: AssetRequest): 

152 req_path = str(p.get('path') or '') 

153 if not req_path: 

154 raise gws.NotFoundError('no path provided') 

155 

156 site_assets = req.site.assetsRoot 

157 

158 project = None 

159 project_assets = None 

160 

161 project_uid = p.get('projectUid') 

162 if project_uid: 

163 project = req.user.require_project(project_uid) 

164 project_assets = project.assetsRoot 

165 

166 real_path = None 

167 

168 if project_assets: 

169 real_path = gws.lib.osx.abs_web_path(req_path, project_assets.dir) 

170 if not real_path and site_assets: 

171 real_path = gws.lib.osx.abs_web_path(req_path, site_assets.dir) 

172 if not real_path: 

173 raise gws.NotFoundError(f'no real path for {req_path=}') 

174 

175 tpl = self.root.app.templateMgr.template_from_path(real_path) 

176 if tpl: 

177 locale = gws.lib.intl.locale(p.localeUid, project.localeUids if project else self.root.app.localeUids) 

178 return self._serve_template(req, tpl, project, locale) 

179 

180 mime = gws.lib.mime.for_path(real_path) 

181 

182 if not _valid_mime_type(mime, project_assets, site_assets): 

183 # NB: pretend the file doesn't exist 

184 raise gws.NotFoundError(f'invalid mime path={real_path!r} mime={mime!r}') 

185 

186 gws.log.debug(f'serving {real_path!r} for {req_path!r}') 

187 return gws.ContentResponse(contentPath=real_path, mime=mime) 

188 

189 def _serve_template(self, req: gws.WebRequester, tpl: gws.Template, project: Optional[gws.Project], locale: gws.Locale): 

190 projects = [p for p in self.root.app.projects if req.user.can_use(p)] 

191 projects.sort(key=lambda p: p.title.lower()) 

192 

193 args = TemplateArgs( 

194 project=project, 

195 projects=projects, 

196 req=req, 

197 user=req.user, 

198 params=req.params(), 

199 ) 

200 

201 return tpl.render(gws.TemplateRenderInput(args=args, locale=locale)) 

202 

203 

204_DEFAULT_ALLOWED_MIME_TYPES = { 

205 gws.lib.mime.CSS, 

206 gws.lib.mime.CSV, 

207 gws.lib.mime.GEOJSON, 

208 gws.lib.mime.GIF, 

209 gws.lib.mime.GML, 

210 gws.lib.mime.GML3, 

211 gws.lib.mime.GZIP, 

212 gws.lib.mime.HTML, 

213 gws.lib.mime.JPEG, 

214 gws.lib.mime.JS, 

215 gws.lib.mime.JSON, 

216 gws.lib.mime.PDF, 

217 gws.lib.mime.PNG, 

218 gws.lib.mime.SVG, 

219 gws.lib.mime.TTF, 

220 gws.lib.mime.TXT, 

221 gws.lib.mime.XML, 

222 gws.lib.mime.ZIP, 

223} 

224 

225 

226def _valid_mime_type(mt, project_assets: Optional[gws.WebDocumentRoot], site_assets: Optional[gws.WebDocumentRoot]): 

227 if project_assets and project_assets.allowMime: 

228 return mt in project_assets.allowMime 

229 if site_assets and site_assets.allowMime: 

230 return mt in site_assets.allowMime 

231 if mt not in _DEFAULT_ALLOWED_MIME_TYPES: 

232 return False 

233 if project_assets and project_assets.denyMime: 

234 return mt not in project_assets.denyMime 

235 if site_assets and site_assets.denyMime: 

236 return mt not in site_assets.denyMime 

237 return True