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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-03 10:12 +0100
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 AssetResponse(gws.Request):
62 content: str
63 mime: str
66class FileRequest(gws.Request):
67 preview: bool = False
68 modelUid: str
69 fieldName: str
70 featureUid: str
73class Object(gws.base.action.Object):
74 """Web action"""
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)
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
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
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
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)
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 :]
125 if path == 'vendor.js':
126 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'vendor', locale))
128 if path == 'util.js':
129 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'util', locale))
131 if path == 'app.js':
132 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'app', locale))
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)
143 raise gws.NotFoundError(f'invalid system asset: {p.path=}')
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')
150 site_assets = req.site.assetsRoot
152 project = None
153 project_assets = None
155 project_uid = p.get('projectUid')
156 if project_uid:
157 project = req.user.require_project(project_uid)
158 project_assets = project.assetsRoot
160 real_path = None
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=}')
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)
174 mime = gws.lib.mime.for_path(real_path)
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}')
180 gws.log.debug(f'serving {real_path!r} for {req_path!r}')
181 return gws.ContentResponse(contentPath=real_path, mime=mime)
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())
187 args = TemplateArgs(
188 project=project,
189 projects=projects,
190 req=req,
191 user=req.user,
192 params=req.params(),
193 )
195 return tpl.render(gws.TemplateRenderInput(args=args, locale=locale))
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}
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