Coverage for gws-app/gws/base/web/action.py: 52%
154 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Web action for pages, assets and files."""
3from typing import Optional, cast
6import gws
7import gws.base.action
8import gws.base.client.bundles
9import gws.lib.mime
10import gws.lib.osx
11import gws.lib.intl
14class TemplateArgs(gws.TemplateArgs):
15 """Asset template arguments."""
17 project: Optional[gws.Project]
18 """Current project."""
19 projects: list[gws.Project]
20 """List of user projects."""
21 req: gws.WebRequester
22 """Requester object."""
23 user: gws.User
24 """Current user."""
25 params: dict
26 """Request parameters."""
27 locale: gws.Locale
28 """Locale object."""
31@gws.ext.config.action('web')
32class Config(gws.base.action.Config):
33 """Serves web pages, assets and files to the browser."""
35 pass
38@gws.ext.props.action('web')
39class Props(gws.base.action.Props):
40 pass
43class AssetRequest(gws.Request):
44 """Asset request."""
46 path: str
47 """Asset path, relative to the assets directory."""
50class PageRequest(gws.Request):
51 """Page request."""
53 name: str
54 """Page name, ``home`` or ``project``."""
57class AssetResponse(gws.Request):
58 """Asset response for API requests."""
60 content: str
61 """Asset content."""
62 mimeType: str
63 """Asset MIME type."""
66class FileRequest(gws.Request):
67 """Request for a file stored in a feature field."""
69 preview: bool = False
70 """Return a preview instead of the file."""
71 modelUid: str
72 """Uid of the model."""
73 fieldName: str
74 """Name of the file field."""
75 featureUid: str
76 """Uid of the feature."""
79@gws.ext.object.action('web')
80class Object(gws.base.action.Object):
81 """Web action, serves pages, assets and files."""
83 @gws.ext.command.api('webAsset')
84 def api_asset(self, req: gws.WebRequester, p: AssetRequest) -> AssetResponse:
85 """Return an asset of the site or of the project."""
86 res = self._serve_path(req, p)
87 if res.contentPath:
88 res.content = gws.u.read_file_b(res.contentPath)
89 return AssetResponse(content=res.content, mimeType=res.mimeType)
91 @gws.ext.command.get('webAsset')
92 def http_asset(self, req: gws.WebRequester, p: AssetRequest) -> gws.ContentResponse:
93 """Serve an asset of the site or of the project."""
94 res = self._serve_path(req, p)
95 return res
97 @gws.ext.command.get('webPage')
98 def get_page(self, req: gws.WebRequester, p: PageRequest) -> gws.ContentResponse:
99 """Render the application home page or a project page."""
100 tpl = None
101 project = None
103 if p.name == 'home':
104 tpl = self.root.app.templateMgr.find_template('application.home', where=[], user=req.user)
105 if p.name == 'project':
106 project = req.user.require_project(p.projectUid)
107 tpl = self.root.app.templateMgr.find_template('project.home', where=[project], user=req.user)
108 if not tpl:
109 raise gws.NotFoundError('template not found for {p.name=}')
111 return self._serve_template(req, p, tpl, project=project)
113 @gws.ext.command.get('webDownload')
114 def download(self, req: gws.WebRequester, p) -> gws.ContentResponse:
115 """Serve an asset as a download attachment."""
116 res = self._serve_path(req, p)
117 if res.contentPath:
118 res.contentFilename = gws.lib.osx.parse_path(res.contentPath).filename
119 return res
121 @gws.ext.command.get('webFile')
122 def file(self, req: gws.WebRequester, p: FileRequest) -> gws.ContentResponse:
123 """Serve a file stored in a feature field."""
124 model = cast(gws.Model, req.user.require(p.modelUid, gws.ext.object.model, gws.Access.read))
125 field = model.field(p.fieldName)
126 if not field:
127 raise gws.NotFoundError()
128 fn = getattr(field, 'handle_web_file_request', None)
129 if not fn:
130 raise gws.NotFoundError()
131 mc = gws.ModelContext(
132 op=gws.ModelOperation.read,
133 user=req.user,
134 project=req.user.require_project(p.projectUid),
135 maxDepth=0,
136 )
137 res = fn(p.featureUid, p.preview, mc)
138 if not res:
139 raise gws.NotFoundError()
140 return res
142 @gws.ext.command.get('webSystemAsset')
143 def sys_asset(self, req: gws.WebRequester, p: AssetRequest) -> gws.ContentResponse:
144 """Serve a client script or style sheet."""
145 locale = gws.lib.intl.locale(p.localeUid, self.root.app.localeUids)
146 app_templates = f'{gws.c.APP_DIR}/gws/base/application/templates/'
148 # only accept '8.0.0.vendor.js' etc or simply 'vendor.js'
149 path = p.path
150 if path.startswith(self.root.app.version):
151 path = path[len(self.root.app.version) + 1 :]
153 if path == 'vendor.js':
154 return gws.ContentResponse(mimeType=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'vendor', locale))
156 if path == 'home.js':
157 return gws.ContentResponse(mimeType=gws.lib.mime.JS, content=gws.u.read_file(f'{app_templates}/home.js'))
158 # deprecated
159 if path == 'util.js':
160 return gws.ContentResponse(mimeType=gws.lib.mime.JS, content=gws.u.read_file(f'{app_templates}/home.js'))
162 if path == 'home.css':
163 return gws.ContentResponse(mimeType=gws.lib.mime.CSS, content=gws.u.read_file(f'{app_templates}/home.css'))
165 if path == 'app.js':
166 return gws.ContentResponse(mimeType=gws.lib.mime.JS, content=gws.base.client.bundles.javascript(self.root, 'app', locale))
168 if path.endswith('.css'):
169 s = path.split('.')
170 if len(s) != 2:
171 raise gws.NotFoundError(f'invalid css request: {p.path=}')
172 content = gws.base.client.bundles.css(self.root, 'app', s[0])
173 if not content:
174 raise gws.NotFoundError(f'invalid css request: {p.path=}')
175 return gws.ContentResponse(mimeType=gws.lib.mime.CSS, content=content)
177 raise gws.NotFoundError(f'invalid system asset: {p.path=}')
179 def _serve_path(self, req: gws.WebRequester, p: AssetRequest):
180 """Locate an asset in the project or site assets directory and serve or render it."""
181 req_path = str(p.get('path') or '')
182 if not req_path:
183 raise gws.NotFoundError('no path provided')
185 site_assets = req.site.assetsRoot
187 project = None
188 project_assets = None
190 project_uid = p.get('projectUid')
191 if project_uid:
192 project = req.user.require_project(project_uid)
193 project_assets = project.assetsRoot
195 real_path = None
196 tpl = None
198 if project_assets:
199 real_path = gws.lib.osx.abs_web_path(req_path, project_assets.dir)
200 if not real_path and site_assets:
201 real_path = gws.lib.osx.abs_web_path(req_path, site_assets.dir)
203 if real_path:
204 tpl = self.root.app.templateMgr.template_from_path(real_path)
205 # deprecated: support for index.cx.html and project.cx.html
206 elif req_path == 'index.cx.html':
207 tpl = self.root.app.templateMgr.find_template('application.home', where=[], user=req.user)
208 elif req_path == 'project.cx.html':
209 tpl = self.root.app.templateMgr.find_template('project.home', where=[project] if project else [], user=req.user)
211 if tpl:
212 return self._serve_template(req, p, tpl, project)
214 if not real_path:
215 raise gws.NotFoundError(f'no real path for {req_path=}')
217 mime_type = gws.lib.mime.for_path(real_path)
219 if not _valid_mime_type(mime_type, project_assets, site_assets):
220 # NB: pretend the file doesn't exist
221 raise gws.NotFoundError(f'invalid mime path={real_path!r} mime_type={mime_type!r}')
223 gws.log.debug(f'serving {real_path!r} for {req_path!r}')
224 return gws.ContentResponse(contentPath=real_path, mimeType=mime_type)
226 def _serve_template(self, req: gws.WebRequester, p: gws.Request, tpl: gws.Template, project: Optional[gws.Project]):
227 """Render a template with the asset template arguments."""
228 locale = gws.lib.intl.locale(p.localeUid, project.localeUids if project else self.root.app.localeUids)
229 projects = [p for p in self.root.app.projects if req.user.can_use(p)]
230 projects.sort(key=lambda p: p.title.lower())
232 args = TemplateArgs(
233 project=project,
234 projects=projects,
235 req=req,
236 user=req.user,
237 params=req.params(),
238 )
240 return tpl.render(gws.TemplateRenderInput(args=args, locale=locale))
243_DEFAULT_ALLOWED_MIME_TYPES = {
244 gws.lib.mime.CSS,
245 gws.lib.mime.CSV,
246 gws.lib.mime.GEOJSON,
247 gws.lib.mime.GIF,
248 gws.lib.mime.GML,
249 gws.lib.mime.GML3,
250 gws.lib.mime.GZIP,
251 gws.lib.mime.HTML,
252 gws.lib.mime.JPEG,
253 gws.lib.mime.JS,
254 gws.lib.mime.JSON,
255 gws.lib.mime.PDF,
256 gws.lib.mime.PNG,
257 gws.lib.mime.SVG,
258 gws.lib.mime.TTF,
259 gws.lib.mime.TXT,
260 gws.lib.mime.XML,
261 gws.lib.mime.ZIP,
262}
265def _valid_mime_type(mt, project_assets: Optional[gws.WebDocumentRoot], site_assets: Optional[gws.WebDocumentRoot]):
266 """Check a MIME type against the allow and deny lists of the assets directories."""
267 if project_assets and project_assets.allowMime:
268 return mt in project_assets.allowMime
269 if site_assets and site_assets.allowMime:
270 return mt in site_assets.allowMime
271 if mt not in _DEFAULT_ALLOWED_MIME_TYPES:
272 return False
273 if project_assets and project_assets.denyMime:
274 return mt not in project_assets.denyMime
275 if site_assets and site_assets.denyMime:
276 return mt not in site_assets.denyMime
277 return True