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

157 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +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 

19 

20import gws 

21import gws.base.action 

22import gws.base.client.bundles 

23import gws.lib.mime 

24import gws.lib.osx 

25import gws.lib.intl 

26 

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

28 

29 

30class TemplateArgs(gws.TemplateArgs): 

31 """Asset template arguments.""" 

32 

33 project: Optional[gws.Project] 

34 """Current project.""" 

35 projects: list[gws.Project] 

36 """List of user projects.""" 

37 req: gws.WebRequester 

38 """Requester object.""" 

39 user: gws.User 

40 """Current user.""" 

41 params: dict 

42 """Request parameters.""" 

43 locale: gws.Locale 

44 """Locale object.""" 

45 

46 

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

48 """Web action configuration.""" 

49 

50 pass 

51 

52 

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

54 pass 

55 

56 

57class AssetRequest(gws.Request): 

58 path: str 

59 

60 

61class PageRequest(gws.Request): 

62 name: str 

63 

64 

65class AssetResponse(gws.Request): 

66 content: str 

67 mime: str 

68 

69 

70class FileRequest(gws.Request): 

71 preview: bool = False 

72 modelUid: str 

73 fieldName: str 

74 featureUid: str 

75 

76 

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

78 """Web action""" 

79 

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

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

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

83 res = self._serve_path(req, p) 

84 if res.contentPath: 

85 res.content = gws.u.read_file_b(res.contentPath) 

86 return AssetResponse(content=res.content, mime=res.mime) 

87 

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

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

90 res = self._serve_path(req, p) 

91 return res 

92 

93 @gws.ext.command.get('webPage') 

94 def get_page(self, req: gws.WebRequester, p: PageRequest) -> gws.ContentResponse: 

95 tpl = None 

96 project = None 

97 

98 if p.name == 'home': 

99 tpl = self.root.app.templateMgr.find_template('application.home', where=[], user=req.user) 

100 if p.name == 'project': 

101 project = req.user.require_project(p.projectUid) 

102 tpl = self.root.app.templateMgr.find_template('project.home', where=[project], user=req.user) 

103 if not tpl: 

104 raise gws.NotFoundError('template not found for {p.name=}') 

105 

106 return self._serve_template(req, p, tpl, project=project) 

107 

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

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

110 res = self._serve_path(req, p) 

111 if res.contentPath: 

112 res.contentFilename = gws.lib.osx.parse_path(res.contentPath).filename 

113 return res 

114 

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

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

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

118 field = model.field(p.fieldName) 

119 if not field: 

120 raise gws.NotFoundError() 

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

122 if not fn: 

123 raise gws.NotFoundError() 

124 mc = gws.ModelContext( 

125 op=gws.ModelOperation.read, 

126 user=req.user, 

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

128 maxDepth=0, 

129 ) 

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

131 if not res: 

132 raise gws.NotFoundError() 

133 return res 

134 

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

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

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

138 app_templates = f'{gws.c.APP_DIR}/gws/base/application/templates/' 

139 

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

141 path = p.path 

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

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

144 

145 if path == 'vendor.js': 

146 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'vendor', locale)) 

147 

148 if path == 'home.js': 

149 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.u.read_file(f'{app_templates}/home.js')) 

150 # deprecated 

151 if path == 'util.js': 

152 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.u.read_file(f'{app_templates}/home.js')) 

153 

154 if path == 'home.css': 

155 return gws.ContentResponse(mime=gws.lib.mime.CSS, content=gws.u.read_file(f'{app_templates}/home.css')) 

156 

157 if path == 'app.js': 

158 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'app', locale)) 

159 

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

161 s = path.split('.') 

162 if len(s) != 2: 

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

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

165 if not content: 

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

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

168 

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

170 

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

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

173 if not req_path: 

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

175 

176 site_assets = req.site.assetsRoot 

177 

178 project = None 

179 project_assets = None 

180 

181 project_uid = p.get('projectUid') 

182 if project_uid: 

183 project = req.user.require_project(project_uid) 

184 project_assets = project.assetsRoot 

185 

186 real_path = None 

187 tpl = None 

188 

189 if project_assets: 

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

191 if not real_path and site_assets: 

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

193 

194 if real_path: 

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

196 # deprecated: support for index.cx.html and project.cx.html 

197 elif req_path == 'index.cx.html': 

198 tpl = self.root.app.templateMgr.find_template('application.home', where=[], user=req.user) 

199 elif req_path == 'project.cx.html': 

200 tpl = self.root.app.templateMgr.find_template('project.home', where=[project] if project else [], user=req.user) 

201 

202 if tpl: 

203 return self._serve_template(req, p, tpl, project) 

204 

205 if not real_path: 

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

207 

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

209 

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

211 # NB: pretend the file doesn't exist 

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

213 

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

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

216 

217 def _serve_template(self, req: gws.WebRequester, p: gws.Request, tpl: gws.Template, project: Optional[gws.Project]): 

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

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

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

221 

222 args = TemplateArgs( 

223 project=project, 

224 projects=projects, 

225 req=req, 

226 user=req.user, 

227 params=req.params(), 

228 ) 

229 

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

231 

232 

233_DEFAULT_ALLOWED_MIME_TYPES = { 

234 gws.lib.mime.CSS, 

235 gws.lib.mime.CSV, 

236 gws.lib.mime.GEOJSON, 

237 gws.lib.mime.GIF, 

238 gws.lib.mime.GML, 

239 gws.lib.mime.GML3, 

240 gws.lib.mime.GZIP, 

241 gws.lib.mime.HTML, 

242 gws.lib.mime.JPEG, 

243 gws.lib.mime.JS, 

244 gws.lib.mime.JSON, 

245 gws.lib.mime.PDF, 

246 gws.lib.mime.PNG, 

247 gws.lib.mime.SVG, 

248 gws.lib.mime.TTF, 

249 gws.lib.mime.TXT, 

250 gws.lib.mime.XML, 

251 gws.lib.mime.ZIP, 

252} 

253 

254 

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

256 if project_assets and project_assets.allowMime: 

257 return mt in project_assets.allowMime 

258 if site_assets and site_assets.allowMime: 

259 return mt in site_assets.allowMime 

260 if mt not in _DEFAULT_ALLOWED_MIME_TYPES: 

261 return False 

262 if project_assets and project_assets.denyMime: 

263 return mt not in project_assets.denyMime 

264 if site_assets and site_assets.denyMime: 

265 return mt not in site_assets.denyMime 

266 return True