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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""Handle dynamic assets.
3An asset is a file located in a global or project-specific ``assets`` directory.
5In order to access a project asset, the user must have ``read`` permission for the project itself.
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.
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.
14If the file is not a template and matches the ``allowMime/denyMime`` filter, its content is returned to the user.
15"""
17from typing import Optional, cast
20import gws
21import gws.base.action
22import gws.base.client.bundles
23import gws.lib.mime
24import gws.lib.osx
25import gws.lib.intl
27gws.ext.new.action('web')
30class TemplateArgs(gws.TemplateArgs):
31 """Asset template arguments."""
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."""
47class Config(gws.base.action.Config):
48 """Web action configuration."""
50 pass
53class Props(gws.base.action.Props):
54 pass
57class AssetRequest(gws.Request):
58 path: str
61class PageRequest(gws.Request):
62 name: str
65class AssetResponse(gws.Request):
66 content: str
67 mime: str
70class FileRequest(gws.Request):
71 preview: bool = False
72 modelUid: str
73 fieldName: str
74 featureUid: str
77class Object(gws.base.action.Object):
78 """Web action"""
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)
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
93 @gws.ext.command.get('webPage')
94 def get_page(self, req: gws.WebRequester, p: PageRequest) -> gws.ContentResponse:
95 tpl = None
96 project = None
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=}')
106 return self._serve_template(req, p, tpl, project=project)
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
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
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/'
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 :]
145 if path == 'vendor.js':
146 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'vendor', locale))
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'))
154 if path == 'home.css':
155 return gws.ContentResponse(mime=gws.lib.mime.CSS, content=gws.u.read_file(f'{app_templates}/home.css'))
157 if path == 'app.js':
158 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'app', locale))
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)
169 raise gws.NotFoundError(f'invalid system asset: {p.path=}')
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')
176 site_assets = req.site.assetsRoot
178 project = None
179 project_assets = None
181 project_uid = p.get('projectUid')
182 if project_uid:
183 project = req.user.require_project(project_uid)
184 project_assets = project.assetsRoot
186 real_path = None
187 tpl = None
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)
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)
202 if tpl:
203 return self._serve_template(req, p, tpl, project)
205 if not real_path:
206 raise gws.NotFoundError(f'no real path for {req_path=}')
208 mime = gws.lib.mime.for_path(real_path)
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}')
214 gws.log.debug(f'serving {real_path!r} for {req_path!r}')
215 return gws.ContentResponse(contentPath=real_path, mime=mime)
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())
222 args = TemplateArgs(
223 project=project,
224 projects=projects,
225 req=req,
226 user=req.user,
227 params=req.params(),
228 )
230 return tpl.render(gws.TemplateRenderInput(args=args, locale=locale))
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}
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