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

131 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 22:59 +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 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 r = self._serve_path(req, p) 

80 if r.contentPath: 

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

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

83 

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

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

86 r = self._serve_path(req, p) 

87 return r 

88 

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

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

91 r = self._serve_path(req, p) 

92 r.asAttachment = True 

93 return r 

94 

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

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

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

98 field = model.field(p.fieldName) 

99 if not field: 

100 raise gws.NotFoundError() 

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

102 if not fn: 

103 raise gws.NotFoundError() 

104 mc = gws.ModelContext( 

105 op=gws.ModelOperation.read, 

106 user=req.user, 

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

108 maxDepth=0, 

109 ) 

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

111 if not res: 

112 raise gws.NotFoundError() 

113 return res 

114 

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

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

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

118 

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

120 path = p.path 

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

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

123 

124 if path == 'vendor.js': 

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

126 

127 if path == 'util.js': 

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

129 

130 if path == 'app.js': 

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

132 

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

134 s = path.split('.') 

135 if len(s) != 2: 

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

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

138 if not content: 

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

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

141 

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

143 

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

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

146 if not req_path: 

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

148 

149 site_assets = req.site.assetsRoot 

150 

151 project = None 

152 project_assets = None 

153 

154 project_uid = p.get('projectUid') 

155 if project_uid: 

156 project = req.user.require_project(project_uid) 

157 project_assets = project.assetsRoot 

158 

159 real_path = None 

160 

161 if project_assets: 

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

163 if not real_path and site_assets: 

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

165 if not real_path: 

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

167 

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

169 if tpl: 

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

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

172 

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

174 

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

176 # NB: pretend the file doesn't exist 

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

178 

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

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

181 

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

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

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

185 

186 args = TemplateArgs( 

187 project=project, 

188 projects=projects, 

189 req=req, 

190 user=req.user, 

191 params=req.params(), 

192 ) 

193 

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

195 

196 

197_DEFAULT_ALLOWED_MIME_TYPES = { 

198 gws.lib.mime.CSS, 

199 gws.lib.mime.CSV, 

200 gws.lib.mime.GEOJSON, 

201 gws.lib.mime.GIF, 

202 gws.lib.mime.GML, 

203 gws.lib.mime.GML3, 

204 gws.lib.mime.GZIP, 

205 gws.lib.mime.HTML, 

206 gws.lib.mime.JPEG, 

207 gws.lib.mime.JS, 

208 gws.lib.mime.JSON, 

209 gws.lib.mime.PDF, 

210 gws.lib.mime.PNG, 

211 gws.lib.mime.SVG, 

212 gws.lib.mime.TTF, 

213 gws.lib.mime.TXT, 

214 gws.lib.mime.XML, 

215 gws.lib.mime.ZIP, 

216} 

217 

218 

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

220 if project_assets and project_assets.allowMime: 

221 return mt in project_assets.allowMime 

222 if site_assets and site_assets.allowMime: 

223 return mt in site_assets.allowMime 

224 if mt not in _DEFAULT_ALLOWED_MIME_TYPES: 

225 return False 

226 if project_assets and project_assets.denyMime: 

227 return mt not in project_assets.denyMime 

228 if site_assets and site_assets.denyMime: 

229 return mt not in site_assets.denyMime 

230 return True