plone.tiles 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ ZCML directive
2
+ ==============
3
+
4
+ A tile is really just a browser view providing ``IBasicTile``
5
+ (or, more commonly, ``ITile`` or ``IPersistentTile``)
6
+ coupled with a named utility providing ``ITileType``.
7
+ The names of the browser view and the tile should match.
8
+
9
+ To make it easier to register these components,
10
+ this package provides a ``<plone:tile />`` directive that sets up both.
11
+ It supports several use cases:
12
+
13
+ * Registering a new tile from a class
14
+ * Registering a new tile from a template only
15
+ * Registering a new tile form a class and a template
16
+ * Registering a new tile for an existing tile type (e.g. for a new layer)
17
+
18
+ To test this,
19
+ we have created a dummy schema and a dummy tile in ``testing.py``,
20
+ and a dummy template in ``test.pt``.
21
+
22
+ Let's show how these may be used by registering several tiles:
23
+
24
+ .. code-block:: python
25
+
26
+ >>> configuration = """\
27
+ ... <configure package="plone.tiles"
28
+ ... xmlns="http://namespaces.zope.org/zope"
29
+ ... xmlns:plone="http://namespaces.plone.org/plone"
30
+ ... i18n_domain="plone.tiles.tests">
31
+ ...
32
+ ... <include package="zope.component" file="meta.zcml" />
33
+ ... <include package="zope.security" file="meta.zcml" />
34
+ ... <include package="zope.browserpage" file="meta.zcml" />
35
+ ...
36
+ ... <include package="plone.tiles" file="meta.zcml" />
37
+ ... <include package="plone.tiles" />
38
+ ...
39
+ ... <permission
40
+ ... id="plone.tiles.testing.DummyAdd"
41
+ ... title="Dummy add permission"
42
+ ... />
43
+ ... <permission
44
+ ... id="plone.tiles.testing.DummyView"
45
+ ... title="Dummy view permission"
46
+ ... />
47
+ ...
48
+ ... <!-- A tile configured with all available attributes -->
49
+ ... <plone:tile
50
+ ... name="dummy1"
51
+ ... title="Dummy tile 1"
52
+ ... description="This one shows all available options"
53
+ ... add_permission="plone.tiles.testing.DummyAdd"
54
+ ... schema="plone.tiles.testing.IDummySchema"
55
+ ... class="plone.tiles.testing.DummyTileWithTemplate"
56
+ ... template="test.pt"
57
+ ... for="plone.tiles.testing.IDummyContext"
58
+ ... layer="plone.tiles.testing.IDummyLayer"
59
+ ... permission="plone.tiles.testing.DummyView"
60
+ ... />
61
+ ...
62
+ ... <!-- A class-only tile -->
63
+ ... <plone:tile
64
+ ... name="dummy2"
65
+ ... title="Dummy tile 2"
66
+ ... add_permission="plone.tiles.testing.DummyAdd"
67
+ ... class="plone.tiles.testing.DummyTile"
68
+ ... for="*"
69
+ ... permission="plone.tiles.testing.DummyView"
70
+ ... />
71
+ ...
72
+ ... <!-- A template-only tile -->
73
+ ... <plone:tile
74
+ ... name="dummy3"
75
+ ... title="Dummy tile 3"
76
+ ... add_permission="plone.tiles.testing.DummyAdd"
77
+ ... template="test.pt"
78
+ ... for="*"
79
+ ... permission="plone.tiles.testing.DummyView"
80
+ ... />
81
+ ...
82
+ ... <!-- Use the PersistentTile class directly with a template-only tile -->
83
+ ... <plone:tile
84
+ ... name="dummy4"
85
+ ... title="Dummy tile 4"
86
+ ... add_permission="plone.tiles.testing.DummyAdd"
87
+ ... schema="plone.tiles.testing.IDummySchema"
88
+ ... class="plone.tiles.PersistentTile"
89
+ ... template="test.pt"
90
+ ... for="*"
91
+ ... permission="plone.tiles.testing.DummyView"
92
+ ... />
93
+ ...
94
+ ... <!-- Override dummy3 for a new layer -->
95
+ ... <plone:tile
96
+ ... name="dummy3"
97
+ ... class="plone.tiles.testing.DummyTile"
98
+ ... for="*"
99
+ ... layer="plone.tiles.testing.IDummyLayer"
100
+ ... permission="plone.tiles.testing.DummyView"
101
+ ... />
102
+ ...
103
+ ... </configure>
104
+ ... """
105
+
106
+ >>> from io import StringIO
107
+ >>> from zope.configuration import xmlconfig
108
+ >>> xmlconfig.xmlconfig(StringIO(configuration))
109
+
110
+ Let's check how the tiles were registered:
111
+
112
+ .. code-block:: python
113
+
114
+ >>> from zope.component import getUtility
115
+ >>> from plone.tiles.interfaces import ITileType
116
+
117
+ >>> tile1_type = getUtility(ITileType, name=u'dummy1')
118
+ >>> tile1_type
119
+ <TileType dummy1 (Dummy tile 1)>
120
+ >>> tile1_type.description
121
+ 'This one shows all available options'
122
+
123
+ >>> tile1_type.add_permission
124
+ 'plone.tiles.testing.DummyAdd'
125
+
126
+ >>> tile1_type.view_permission
127
+ 'plone.tiles.testing.DummyView'
128
+
129
+ >>> tile1_type.schema
130
+ <InterfaceClass plone.tiles.testing.IDummySchema>
131
+
132
+ >>> tile2_type = getUtility(ITileType, name=u'dummy2')
133
+ >>> tile2_type
134
+ <TileType dummy2 (Dummy tile 2)>
135
+ >>> tile2_type.description is None
136
+ True
137
+ >>> tile2_type.add_permission
138
+ 'plone.tiles.testing.DummyAdd'
139
+ >>> tile2_type.schema is None
140
+ True
141
+
142
+ >>> tile3_type = getUtility(ITileType, name=u'dummy3')
143
+ >>> tile3_type
144
+ <TileType dummy3 (Dummy tile 3)>
145
+ >>> tile3_type.description is None
146
+ True
147
+ >>> tile3_type.add_permission
148
+ 'plone.tiles.testing.DummyAdd'
149
+ >>> tile3_type.schema is None
150
+ True
151
+
152
+ >>> tile4_type = getUtility(ITileType, name=u'dummy4')
153
+ >>> tile4_type
154
+ <TileType dummy4 (Dummy tile 4)>
155
+ >>> tile4_type.description is None
156
+ True
157
+ >>> tile4_type.add_permission
158
+ 'plone.tiles.testing.DummyAdd'
159
+ >>> tile4_type.schema
160
+ <InterfaceClass plone.tiles.testing.IDummySchema>
161
+
162
+ Finally, let's check that we can look up the tiles:
163
+
164
+ .. code-block:: python
165
+
166
+ >>> from zope.publisher.browser import TestRequest
167
+ >>> from zope.interface import implementer, alsoProvides
168
+
169
+ >>> from plone.tiles.testing import IDummyContext, IDummyLayer
170
+
171
+ >>> @implementer(IDummyContext)
172
+ ... class Context(object):
173
+ ... pass
174
+
175
+ >>> context = Context()
176
+ >>> request = TestRequest()
177
+ >>> layer_request = TestRequest(skin=IDummyLayer)
178
+
179
+ >>> from zope.component import getMultiAdapter
180
+ >>> from plone.tiles import Tile, PersistentTile
181
+ >>> from plone.tiles.testing import DummyTile, DummyTileWithTemplate
182
+
183
+ >>> tile1 = getMultiAdapter((context, layer_request), name='dummy1')
184
+ >>> isinstance(tile1, DummyTileWithTemplate)
185
+ True
186
+ >>> print(tile1().strip())
187
+ <b>test!</b>
188
+ >>> tile1.__name__
189
+ 'dummy1'
190
+
191
+ >>> tile2 = getMultiAdapter((context, request), name='dummy2')
192
+ >>> isinstance(tile2, DummyTile)
193
+ True
194
+ >>> print(tile2())
195
+ dummy
196
+ >>> tile2.__name__
197
+ 'dummy2'
198
+
199
+ >>> tile3 = getMultiAdapter((context, request), name='dummy3')
200
+ >>> isinstance(tile3, Tile)
201
+ True
202
+ >>> print(tile3().strip())
203
+ <b>test!</b>
204
+ >>> tile3.__name__
205
+ 'dummy3'
206
+
207
+ >>> tile4 = getMultiAdapter((context, request), name='dummy4')
208
+ >>> isinstance(tile4, PersistentTile)
209
+ True
210
+ >>> print(tile4().strip())
211
+ <b>test!</b>
212
+ >>> tile4.__name__
213
+ 'dummy4'
214
+
215
+ >>> tile3_layer = getMultiAdapter((context, layer_request), name='dummy3')
216
+ >>> isinstance(tile3_layer, DummyTile)
217
+ True
218
+ >>> print(tile3_layer())
219
+ dummy
220
+ >>> tile3_layer.__name__
221
+ 'dummy3'
plone/tiles/esi.py ADDED
@@ -0,0 +1,214 @@
1
+ from plone.tiles.interfaces import ESI_HEADER
2
+ from plone.tiles.interfaces import ESI_HEADER_KEY
3
+ from plone.tiles.interfaces import IESIRendered
4
+ from plone.tiles.interfaces import ITileType
5
+ from plone.tiles.tile import PersistentTile
6
+ from plone.tiles.tile import Tile
7
+ from Products.Five import BrowserView
8
+ from zExceptions import Unauthorized
9
+ from zope.component import queryUtility
10
+ from zope.interface import implementer
11
+
12
+ import os
13
+ import re
14
+ import transaction
15
+
16
+
17
+ try:
18
+ from AccessControl.security import checkPermission
19
+ except ImportError:
20
+ from zope.security import checkPermission
21
+
22
+
23
+ X_FRAME_OPTIONS = os.environ.get("PLONE_X_FRAME_OPTIONS", "SAMEORIGIN")
24
+
25
+ HEAD_CHILDREN = re.compile(r"<head[^>]*>(.*)</head>", re.I | re.S)
26
+ BODY_CHILDREN = re.compile(r"<body[^>]*>(.*)</body>", re.I | re.S)
27
+
28
+ ESI_NAMESPACE_MAP = {"esi": "http://www.edge-delivery.org/esi/1.0"}
29
+ _ESI_HREF = 'href="{url}/@@{esiMode}?{queryString}"'
30
+ ESI_TEMPLATE = (
31
+ """\
32
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
33
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
34
+ <html xmlns="http://www.w3.org/1999/xhtml">
35
+ <body>
36
+ <a class="_esi_placeholder" rel="esi" """
37
+ + _ESI_HREF
38
+ + """></a>
39
+ </body>
40
+ </html>
41
+ """
42
+ )
43
+
44
+
45
+ def substituteESILinks(rendered):
46
+ """Turn ESI links like <a class="_esi_placeholder" rel="esi" href="..." />
47
+ into <esi:include /> links.
48
+
49
+ ``rendered`` should be an HTML string.
50
+ """
51
+
52
+ rendered = re.sub(
53
+ r"<html", '<html xmlns:esi="{}"'.format(ESI_NAMESPACE_MAP["esi"]), rendered, 1
54
+ )
55
+ return re.sub(
56
+ r'<a class="_esi_placeholder" rel="esi" href="([^"]+)"></a>',
57
+ r'<esi:include src="\1" />',
58
+ rendered,
59
+ )
60
+
61
+
62
+ class ConditionalESIRendering:
63
+ head = False
64
+
65
+ def render(self):
66
+ raise NotImplementedError(
67
+ "Override render() or set a class variable 'index' to point to "
68
+ "a view page template file"
69
+ )
70
+
71
+ def __call__(self, *args, **kwargs):
72
+ if self.request.getHeader(ESI_HEADER, "false").lower() == "true":
73
+ mode = "esi-body"
74
+ if self.head:
75
+ mode = "esi-head"
76
+ return ESI_TEMPLATE.format(
77
+ url=(
78
+ self.request.get("PATH_INFO")
79
+ and self.request.get("PATH_INFO").replace(" ", "%20")
80
+ or self.request.getURL()
81
+ ),
82
+ queryString=self.request.get("QUERY_STRING", ""),
83
+ esiMode=mode,
84
+ )
85
+ # Do not hide AttributeError inside index()
86
+ try:
87
+ self.index
88
+ except AttributeError:
89
+ return self.render()
90
+ return self.index(*args, **kwargs)
91
+
92
+
93
+ # Convenience base classes
94
+
95
+
96
+ @implementer(IESIRendered)
97
+ class ESITile(ConditionalESIRendering, Tile):
98
+ """Convenience class for tiles using ESI rendering.
99
+
100
+ Set ``head`` to True if this tile renders <head /> content. The
101
+ default is to render <body /> content.
102
+ """
103
+
104
+ head = False
105
+
106
+
107
+ @implementer(IESIRendered)
108
+ class ESIPersistentTile(ConditionalESIRendering, PersistentTile):
109
+ """Convenience class for tiles using ESI rendering.
110
+
111
+ Set ``head`` to True if this tile renders <head /> content. The
112
+ default is to render <body /> content.
113
+ """
114
+
115
+ head = False
116
+
117
+
118
+ # ESI views
119
+
120
+
121
+ class ESIHead(BrowserView):
122
+ """Render the head portion of a tile independently."""
123
+
124
+ def __call__(self):
125
+ """Return the children of the <head> tag as a fragment."""
126
+ # Check for the registered view permission
127
+ try:
128
+ type_ = queryUtility(ITileType, self.context.__name__)
129
+ permission = type_.view_permission
130
+ except AttributeError:
131
+ permission = None
132
+ if permission:
133
+ if not checkPermission(permission, self.context):
134
+ raise Unauthorized()
135
+
136
+ if self.request.getHeader(ESI_HEADER):
137
+ del self.request.environ[ESI_HEADER_KEY]
138
+
139
+ document = self.context() # render the tile
140
+
141
+ # Disable the theme so we don't <html/>-wrapped
142
+ self.request.response.setHeader("X-Theme-Disabled", "1")
143
+
144
+ match = HEAD_CHILDREN.search(document)
145
+ if not match:
146
+ return document
147
+ return match.group(1).strip()
148
+
149
+
150
+ class ESIBody(BrowserView):
151
+ """Render the head portion of a tile independently."""
152
+
153
+ def __call__(self):
154
+ """Return the children of the <head> tag as a fragment."""
155
+ # Check for the registered view permission
156
+ try:
157
+ type_ = queryUtility(ITileType, self.context.__name__)
158
+ permission = type_.view_permission
159
+ except AttributeError:
160
+ permission = None
161
+ if permission:
162
+ if not checkPermission(permission, self.context):
163
+ raise Unauthorized()
164
+
165
+ if self.request.getHeader(ESI_HEADER):
166
+ del self.request.environ[ESI_HEADER_KEY]
167
+
168
+ document = self.context() # render the tile
169
+
170
+ # Disable the theme so we don't <html/>-wrapped
171
+ self.request.response.setHeader("X-Theme-Disabled", "1")
172
+
173
+ match = BODY_CHILDREN.search(document)
174
+ if not match:
175
+ return document
176
+ return match.group(1).strip()
177
+
178
+
179
+ class ESIProtectTransform:
180
+ """Replacement transform for plone.protect's ProtectTransform,
181
+ because ESI tile responses' HTML should not be transformed to
182
+ avoid wrapping them with <html>-tag
183
+ """
184
+
185
+ order = 9000
186
+
187
+ def __init__(self, published, request):
188
+ self.published = published
189
+ self.request = request
190
+
191
+ def transform(self, result, encoding):
192
+ from plone.protect.interfaces import IDisableCSRFProtection
193
+
194
+ # clickjacking protection from plone.protect
195
+ if X_FRAME_OPTIONS:
196
+ if not self.request.response.getHeader("X-Frame-Options"):
197
+ self.request.response.setHeader("X-Frame-Options", X_FRAME_OPTIONS)
198
+ # drop X-Tile-Url
199
+ if "x-tile-url" in self.request.response.headers:
200
+ del self.request.response.headers["x-tile-url"]
201
+ # ESI requests are always GET request and should not mutate DB
202
+ # unless they provide IDisableCSRFProtection
203
+ if not IDisableCSRFProtection.providedBy(self.request):
204
+ transaction.abort()
205
+ return None
206
+
207
+ def transformBytes(self, result, encoding):
208
+ return self.transform(result, encoding)
209
+
210
+ def transformUnicode(self, result, encoding):
211
+ return self.transform(result, encoding)
212
+
213
+ def transformIterable(self, result, encoding):
214
+ return self.transform(result, encoding)