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.
- plone/tiles/__init__.py +7 -0
- plone/tiles/absoluteurl.py +101 -0
- plone/tiles/configure.zcml +184 -0
- plone/tiles/data.py +422 -0
- plone/tiles/data.rst +179 -0
- plone/tiles/directives.py +13 -0
- plone/tiles/directives.rst +221 -0
- plone/tiles/esi.py +214 -0
- plone/tiles/esi.rst +325 -0
- plone/tiles/fieldtypeconverters.py +39 -0
- plone/tiles/interfaces.py +172 -0
- plone/tiles/meta.py +162 -0
- plone/tiles/meta.zcml +16 -0
- plone/tiles/test.pt +1 -0
- plone/tiles/testing.py +59 -0
- plone/tiles/tests/__init__.py +0 -0
- plone/tiles/tests/test_data.py +83 -0
- plone/tiles/tests/test_doctests.py +30 -0
- plone/tiles/tile.py +186 -0
- plone/tiles/tiles.rst +783 -0
- plone/tiles/type.py +39 -0
- plone.tiles-3.0.0-py3.13-nspkg.pth +1 -0
- plone_tiles-3.0.0.dist-info/LICENSE.GPL +339 -0
- plone_tiles-3.0.0.dist-info/LICENSE.txt +16 -0
- plone_tiles-3.0.0.dist-info/METADATA +1990 -0
- plone_tiles-3.0.0.dist-info/RECORD +29 -0
- plone_tiles-3.0.0.dist-info/WHEEL +5 -0
- plone_tiles-3.0.0.dist-info/namespace_packages.txt +1 -0
- plone_tiles-3.0.0.dist-info/top_level.txt +1 -0
@@ -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)
|