Coverage for gws-app/gws/base/web/action.py: 0%
134 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +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
19import os
20import re
22import gws
23import gws.base.action
24import gws.base.client.bundles
25import gws.base.template
26import gws.lib.mime
27import gws.lib.osx
28import gws.lib.intl
30gws.ext.new.action('web')
33class TemplateArgs(gws.TemplateArgs):
34 """Asset template arguments."""
36 project: Optional[gws.Project]
37 """Current project."""
38 projects: list[gws.Project]
39 """List of user projects."""
40 req: gws.WebRequester
41 """Requester object."""
42 user: gws.User
43 """Current user."""
44 params: dict
45 """Request parameters."""
46 locale: gws.Locale
47 """Locale object."""
50class Config(gws.base.action.Config):
51 pass
54class Props(gws.base.action.Props):
55 pass
58class AssetRequest(gws.Request):
59 path: str
62class AssetResponse(gws.Request):
63 content: str
64 mime: str
67class FileRequest(gws.Request):
68 preview: bool = False
69 modelUid: str
70 fieldName: str
71 featureUid: str
74class Object(gws.base.action.Object):
75 """Web action"""
77 @gws.ext.command.api('webAsset')
78 def api_asset(self, req: gws.WebRequester, p: AssetRequest) -> AssetResponse:
79 """Return an asset under the given path and project"""
80 r = self._serve_path(req, p)
81 if r.contentPath:
82 r.content = gws.u.read_file_b(r.contentPath)
83 return AssetResponse(content=r.content, mime=r.mime)
85 @gws.ext.command.get('webAsset')
86 def http_asset(self, req: gws.WebRequester, p: AssetRequest) -> gws.ContentResponse:
87 r = self._serve_path(req, p)
88 return r
90 @gws.ext.command.get('webDownload')
91 def download(self, req: gws.WebRequester, p) -> gws.ContentResponse:
92 r = self._serve_path(req, p)
93 r.asAttachment = True
94 return r
96 @gws.ext.command.get('webFile')
97 def file(self, req: gws.WebRequester, p: FileRequest) -> gws.ContentResponse:
98 model = cast(gws.Model, req.user.acquire(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(
127 mime=gws.lib.mime.JS,
128 content=gws.base.client.bundles.javascript(self.root, 'vendor', locale))
130 if path == 'util.js':
131 return gws.ContentResponse(
132 mime=gws.lib.mime.JS,
133 content=gws.base.client.bundles.javascript(self.root, 'util', locale))
135 if path == 'app.js':
136 return gws.ContentResponse(
137 mime=gws.lib.mime.JS,
138 content=gws.base.client.bundles.javascript(self.root, 'app', locale))
140 if path.endswith('.css'):
141 s = path.split('.')
142 if len(s) != 2:
143 raise gws.NotFoundError(f'invalid css request: {p.path=}')
144 content = gws.base.client.bundles.css(self.root, 'app', s[0])
145 if not content:
146 raise gws.NotFoundError(f'invalid css request: {p.path=}')
147 return gws.ContentResponse(mime=gws.lib.mime.CSS, content=content)
149 raise gws.NotFoundError(f'invalid system asset: {p.path=}')
151 def _serve_path(self, req: gws.WebRequester, p: AssetRequest):
152 req_path = str(p.get('path') or '')
153 if not req_path:
154 raise gws.NotFoundError('no path provided')
156 site_assets = req.site.assetsRoot
158 project = None
159 project_assets = None
161 project_uid = p.get('projectUid')
162 if project_uid:
163 project = req.user.require_project(project_uid)
164 project_assets = project.assetsRoot
166 real_path = None
168 if project_assets:
169 real_path = gws.lib.osx.abs_web_path(req_path, project_assets.dir)
170 if not real_path and site_assets:
171 real_path = gws.lib.osx.abs_web_path(req_path, site_assets.dir)
172 if not real_path:
173 raise gws.NotFoundError(f'no real path for {req_path=}')
175 tpl = self.root.app.templateMgr.template_from_path(real_path)
176 if tpl:
177 locale = gws.lib.intl.locale(p.localeUid, project.localeUids if project else self.root.app.localeUids)
178 return self._serve_template(req, tpl, project, locale)
180 mime = gws.lib.mime.for_path(real_path)
182 if not _valid_mime_type(mime, project_assets, site_assets):
183 # NB: pretend the file doesn't exist
184 raise gws.NotFoundError(f'invalid mime path={real_path!r} mime={mime!r}')
186 gws.log.debug(f'serving {real_path!r} for {req_path!r}')
187 return gws.ContentResponse(contentPath=real_path, mime=mime)
189 def _serve_template(self, req: gws.WebRequester, tpl: gws.Template, project: Optional[gws.Project], locale: gws.Locale):
190 projects = [p for p in self.root.app.projects if req.user.can_use(p)]
191 projects.sort(key=lambda p: p.title.lower())
193 args = TemplateArgs(
194 project=project,
195 projects=projects,
196 req=req,
197 user=req.user,
198 params=req.params(),
199 )
201 return tpl.render(gws.TemplateRenderInput(args=args, locale=locale))
204_DEFAULT_ALLOWED_MIME_TYPES = {
205 gws.lib.mime.CSS,
206 gws.lib.mime.CSV,
207 gws.lib.mime.GEOJSON,
208 gws.lib.mime.GIF,
209 gws.lib.mime.GML,
210 gws.lib.mime.GML3,
211 gws.lib.mime.GZIP,
212 gws.lib.mime.HTML,
213 gws.lib.mime.JPEG,
214 gws.lib.mime.JS,
215 gws.lib.mime.JSON,
216 gws.lib.mime.PDF,
217 gws.lib.mime.PNG,
218 gws.lib.mime.SVG,
219 gws.lib.mime.TTF,
220 gws.lib.mime.TXT,
221 gws.lib.mime.XML,
222 gws.lib.mime.ZIP,
223}
226def _valid_mime_type(mt, project_assets: Optional[gws.WebDocumentRoot], site_assets: Optional[gws.WebDocumentRoot]):
227 if project_assets and project_assets.allowMime:
228 return mt in project_assets.allowMime
229 if site_assets and site_assets.allowMime:
230 return mt in site_assets.allowMime
231 if mt not in _DEFAULT_ALLOWED_MIME_TYPES:
232 return False
233 if project_assets and project_assets.denyMime:
234 return mt not in project_assets.denyMime
235 if site_assets and site_assets.denyMime:
236 return mt not in site_assets.denyMime
237 return True