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 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +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 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 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)
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
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
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
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)
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 :]
124 if path == 'vendor.js':
125 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'vendor', locale))
127 if path == 'util.js':
128 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'util', locale))
130 if path == 'app.js':
131 return gws.ContentResponse(mime=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'app', locale))
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)
142 raise gws.NotFoundError(f'invalid system asset: {p.path=}')
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')
149 site_assets = req.site.assetsRoot
151 project = None
152 project_assets = None
154 project_uid = p.get('projectUid')
155 if project_uid:
156 project = req.user.require_project(project_uid)
157 project_assets = project.assetsRoot
159 real_path = None
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=}')
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)
173 mime = gws.lib.mime.for_path(real_path)
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}')
179 gws.log.debug(f'serving {real_path!r} for {req_path!r}')
180 return gws.ContentResponse(contentPath=real_path, mime=mime)
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())
186 args = TemplateArgs(
187 project=project,
188 projects=projects,
189 req=req,
190 user=req.user,
191 params=req.params(),
192 )
194 return tpl.render(gws.TemplateRenderInput(args=args, locale=locale))
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}
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