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

132 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-03 10:12 +0100

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 AssetResponse(gws.Request): 

62 content: str 

63 mime: str 

64 

65 

66class FileRequest(gws.Request): 

67 preview: bool = False 

68 modelUid: str 

69 fieldName: str 

70 featureUid: str 

71 

72 

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

74 """Web action""" 

75 

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

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

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

79 res = self._serve_path(req, p) 

80 if res.contentPath: 

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

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

83 

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

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

86 res = self._serve_path(req, p) 

87 return res 

88 

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

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

91 res = self._serve_path(req, p) 

92 pp = gws.lib.osx.parse_path(res.contentPath) 

93 res.contentFilename = pp.filename 

94 return res 

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.require(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(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'vendor', locale)) 

127 

128 if path == 'util.js': 

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

130 

131 if path == 'app.js': 

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

133 

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

135 s = path.split('.') 

136 if len(s) != 2: 

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

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

139 if not content: 

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

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

142 

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

144 

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

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

147 if not req_path: 

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

149 

150 site_assets = req.site.assetsRoot 

151 

152 project = None 

153 project_assets = None 

154 

155 project_uid = p.get('projectUid') 

156 if project_uid: 

157 project = req.user.require_project(project_uid) 

158 project_assets = project.assetsRoot 

159 

160 real_path = None 

161 

162 if project_assets: 

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

164 if not real_path and site_assets: 

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

166 if not real_path: 

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

168 

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

170 if tpl: 

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

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

173 

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

175 

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

177 # NB: pretend the file doesn't exist 

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

179 

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

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

182 

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

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

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

186 

187 args = TemplateArgs( 

188 project=project, 

189 projects=projects, 

190 req=req, 

191 user=req.user, 

192 params=req.params(), 

193 ) 

194 

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

196 

197 

198_DEFAULT_ALLOWED_MIME_TYPES = { 

199 gws.lib.mime.CSS, 

200 gws.lib.mime.CSV, 

201 gws.lib.mime.GEOJSON, 

202 gws.lib.mime.GIF, 

203 gws.lib.mime.GML, 

204 gws.lib.mime.GML3, 

205 gws.lib.mime.GZIP, 

206 gws.lib.mime.HTML, 

207 gws.lib.mime.JPEG, 

208 gws.lib.mime.JS, 

209 gws.lib.mime.JSON, 

210 gws.lib.mime.PDF, 

211 gws.lib.mime.PNG, 

212 gws.lib.mime.SVG, 

213 gws.lib.mime.TTF, 

214 gws.lib.mime.TXT, 

215 gws.lib.mime.XML, 

216 gws.lib.mime.ZIP, 

217} 

218 

219 

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

221 if project_assets and project_assets.allowMime: 

222 return mt in project_assets.allowMime 

223 if site_assets and site_assets.allowMime: 

224 return mt in site_assets.allowMime 

225 if mt not in _DEFAULT_ALLOWED_MIME_TYPES: 

226 return False 

227 if project_assets and project_assets.denyMime: 

228 return mt not in project_assets.denyMime 

229 if site_assets and site_assets.denyMime: 

230 return mt not in site_assets.denyMime 

231 return True