collective.collabora 1.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.
Files changed (47) hide show
  1. collective/collabora/__init__.py +12 -0
  2. collective/collabora/adapters.py +82 -0
  3. collective/collabora/browser/__init__.py +0 -0
  4. collective/collabora/browser/configure.zcml +49 -0
  5. collective/collabora/browser/edit.py +257 -0
  6. collective/collabora/browser/static/edit.css +32 -0
  7. collective/collabora/browser/static/edit.js +92 -0
  8. collective/collabora/browser/templates/edit.pt +105 -0
  9. collective/collabora/browser/wopi.py +205 -0
  10. collective/collabora/configure.zcml +40 -0
  11. collective/collabora/interfaces.py +38 -0
  12. collective/collabora/locales/README.rst +65 -0
  13. collective/collabora/locales/__init__.py +0 -0
  14. collective/collabora/locales/collective.collabora.pot +123 -0
  15. collective/collabora/locales/de/LC_MESSAGES/collective.collabora.po +120 -0
  16. collective/collabora/locales/en/LC_MESSAGES/collective.collabora.po +120 -0
  17. collective/collabora/locales/es/LC_MESSAGES/collective.collabora.po +125 -0
  18. collective/collabora/locales/fr/LC_MESSAGES/collective.collabora.po +120 -0
  19. collective/collabora/locales/nl/LC_MESSAGES/collective.collabora.po +120 -0
  20. collective/collabora/locales/update.py +80 -0
  21. collective/collabora/locales/update.sh +12 -0
  22. collective/collabora/profiles/default/browserlayer.xml +6 -0
  23. collective/collabora/profiles/default/metadata.xml +7 -0
  24. collective/collabora/profiles/default/registry.xml +29 -0
  25. collective/collabora/profiles/default/types/File.xml +32 -0
  26. collective/collabora/profiles/default/types.xml +8 -0
  27. collective/collabora/profiles/uninstall/browserlayer.xml +6 -0
  28. collective/collabora/setuphandlers.py +23 -0
  29. collective/collabora/testdata/server_discovery_xml +467 -0
  30. collective/collabora/testdata/testfile.docx +0 -0
  31. collective/collabora/testing.py +148 -0
  32. collective/collabora/tests/__init__.py +0 -0
  33. collective/collabora/tests/test_adapters.py +58 -0
  34. collective/collabora/tests/test_edit.py +275 -0
  35. collective/collabora/tests/test_setup.py +135 -0
  36. collective/collabora/tests/test_utils.py +90 -0
  37. collective/collabora/tests/test_wopi.py +321 -0
  38. collective/collabora/utils.py +78 -0
  39. collective.collabora-1.0.0-py3.10-nspkg.pth +1 -0
  40. collective.collabora-1.0.0.dist-info/LICENSE.GPL +339 -0
  41. collective.collabora-1.0.0.dist-info/LICENSE.rst +15 -0
  42. collective.collabora-1.0.0.dist-info/METADATA +174 -0
  43. collective.collabora-1.0.0.dist-info/RECORD +47 -0
  44. collective.collabora-1.0.0.dist-info/WHEEL +5 -0
  45. collective.collabora-1.0.0.dist-info/entry_points.txt +5 -0
  46. collective.collabora-1.0.0.dist-info/namespace_packages.txt +1 -0
  47. collective.collabora-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Init and utils."""
3
+ from __future__ import unicode_literals
4
+
5
+ from future import standard_library
6
+
7
+
8
+ standard_library.install_aliases()
9
+ from zope.i18nmessageid import MessageFactory
10
+
11
+
12
+ _ = MessageFactory("collective.collabora")
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import unicode_literals
3
+
4
+ from future import standard_library
5
+
6
+
7
+ standard_library.install_aliases()
8
+
9
+ from collective.collabora import utils
10
+ from collective.collabora.interfaces import IStoredFile
11
+ from plone.app.contenttypes.interfaces import IFile
12
+ from plone.namedfile.file import NamedBlobFile
13
+ from zope.component import adapter
14
+ from zope.interface import implementer
15
+
16
+
17
+ try:
18
+ from Products.ATContentTypes.interfaces import IATFile
19
+ from StringIO import StringIO # py27 only
20
+ except ImportError:
21
+ from collective.collabora.interfaces import IDummy as IATFile
22
+
23
+ StringIO = utils.disallow
24
+
25
+
26
+ @adapter(IFile)
27
+ @implementer(IStoredFile)
28
+ class DXStoredFile(object):
29
+ """Access the file storage on a Dexterity content object.
30
+
31
+ i.e. the file attribute on the content object.
32
+ """
33
+
34
+ def __init__(self, context):
35
+ self.context = context
36
+ self.filename = context.file.filename
37
+ self.contentType = context.file.contentType
38
+
39
+ @property
40
+ def data(self):
41
+ return self.context.file.data
42
+
43
+ @data.setter
44
+ def data(self, data):
45
+ self.context.file = NamedBlobFile(
46
+ data=data, filename=self.filename, contentType=self.contentType
47
+ )
48
+
49
+ def getSize(self):
50
+ return self.context.file.getSize()
51
+
52
+
53
+ @adapter(IATFile)
54
+ @implementer(IStoredFile)
55
+ class ATStoredFile(object):
56
+ """Access the file storage on a Archetypes content object.
57
+
58
+ i.e. the File field on the content object.
59
+ """
60
+
61
+ def __init__(self, context):
62
+ self.context = context
63
+ self.file = context.getField("file")
64
+ # This weirdness is why we adopt the content type, rather than the blob
65
+ # field directly - these accessors on the field need the parent context
66
+ self.filename = self.file.getFilename(context)
67
+ self.contentType = self.file.getContentType(context)
68
+
69
+ @property
70
+ def data(self):
71
+ return self.context.data
72
+
73
+ @data.setter
74
+ def data(self, data):
75
+ # This follows the README.txt in
76
+ # https://github.com/plone/plone.app.blob/tree/master/src/plone/app/blob
77
+ data_wrapper = StringIO(data)
78
+ data_wrapper.filename = self.filename
79
+ self.context.setFile(data_wrapper)
80
+
81
+ def getSize(self):
82
+ return self.file.get_size(self.context)
File without changes
@@ -0,0 +1,49 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:browser="http://namespaces.zope.org/browser"
4
+ xmlns:zcml="http://namespaces.zope.org/zcml"
5
+ i18n_domain="collective.collabora"
6
+ >
7
+
8
+ <browser:resourceDirectory
9
+ name="collective.collabora"
10
+ directory="./static"
11
+ />
12
+
13
+ <browser:page
14
+ name="collabora-edit"
15
+ for="plone.app.contenttypes.interfaces.IFile"
16
+ class=".edit.CollaboraEditView"
17
+ template="templates/edit.pt"
18
+ permission="zope2.View"
19
+ layer="collective.collabora.interfaces.ICollectiveCollaboraLayer"
20
+ />
21
+
22
+ <browser:page
23
+ name="collabora-wopi"
24
+ for="plone.app.contenttypes.interfaces.IFile"
25
+ class=".wopi.CollaboraWOPIView"
26
+ permission="zope2.View"
27
+ layer="collective.collabora.interfaces.ICollectiveCollaboraLayer"
28
+ />
29
+
30
+ <configure zcml:condition="installed Products.ATContentTypes">
31
+ <browser:page
32
+ name="collabora-edit"
33
+ for="Products.ATContentTypes.interfaces.IATFile"
34
+ class=".edit.CollaboraEditView"
35
+ template="templates/edit.pt"
36
+ permission="zope2.View"
37
+ layer="collective.collabora.interfaces.ICollectiveCollaboraLayer"
38
+ />
39
+
40
+ <browser:page
41
+ name="collabora-wopi"
42
+ for="Products.ATContentTypes.interfaces.IATFile"
43
+ class=".wopi.CollaboraWOPIView"
44
+ permission="zope2.View"
45
+ layer="collective.collabora.interfaces.ICollectiveCollaboraLayer"
46
+ />
47
+ </configure>
48
+
49
+ </configure>
@@ -0,0 +1,257 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import unicode_literals
3
+
4
+ from builtins import dict
5
+ from builtins import filter
6
+ from builtins import next
7
+ from builtins import super
8
+ from future import standard_library
9
+ from future.utils import bytes_to_native_str as n
10
+
11
+
12
+ standard_library.install_aliases()
13
+
14
+ from collective.collabora import _
15
+ from collective.collabora import utils
16
+ from collective.collabora.adapters import IStoredFile
17
+ from logging import getLogger
18
+ from lxml import etree
19
+ from plone import api
20
+ from plone.app.contenttypes.browser.file import FileView
21
+ from plone.memoize.view import memoize
22
+ from plone.uuid.interfaces import IUUID
23
+ from Products.PluggableAuthService.interfaces.plugins import IAuthenticationPlugin
24
+ from urllib.parse import urlencode
25
+ from urllib.parse import urlparse
26
+
27
+ import requests
28
+
29
+
30
+ logger = getLogger(__name__)
31
+
32
+ error_server_discovery = _(
33
+ "error_server_discovery", default="Collabora server is not responding."
34
+ )
35
+
36
+
37
+ class CollaboraEditView(FileView):
38
+ """User interface for interacting with Collabora Online."""
39
+
40
+ def __init__(self, context, request):
41
+ # newsuper throws an infinite loop in py27
42
+ super(FileView, self).__init__(context, request)
43
+ self.error_msg = None
44
+ self.stored_file = IStoredFile(context)
45
+
46
+ def __call__(self):
47
+ if not all([self.plone_server_url, self.collabora_server_url, self.wopi_url]):
48
+ # accessing those detects errors and sets self.error_msg
49
+ pass
50
+ return super(FileView, self).__call__()
51
+
52
+ def human_readable_size(self):
53
+ """Use our own reimplementation, against our own adapter"""
54
+ return utils.human_readable_size(self.stored_file.getSize())
55
+
56
+ @property
57
+ @memoize
58
+ def plone_version(self):
59
+ """Get the major version we're running in."""
60
+ return "plone%i" % utils.PLONE_VERSION
61
+
62
+ @property
63
+ @memoize
64
+ def can_edit(self):
65
+ return api.user.has_permission(
66
+ "Modify portal content", user=api.user.get_current(), obj=self.context
67
+ )
68
+
69
+ @property
70
+ @memoize
71
+ def download_url(self):
72
+ return "/".join(
73
+ (
74
+ self.context.absolute_url(),
75
+ "@@download",
76
+ "file",
77
+ self.stored_file.filename,
78
+ )
79
+ )
80
+
81
+ @property
82
+ @memoize
83
+ def portal_url(self):
84
+ """The 'normal' Plone portal url, as used in the"""
85
+ return api.portal.get().absolute_url()
86
+
87
+ @property
88
+ @memoize
89
+ def plone_server_url(self):
90
+ """The base URL for Collabora to reach Plone on"""
91
+ plone_server_url = api.portal.get_registry_record(
92
+ n(b"collective.collabora.plone_server_url"), default=None
93
+ )
94
+ if not plone_server_url:
95
+ plone_server_url = self.portal_url
96
+ parts = urlparse(plone_server_url)
97
+ # This assumes you are running Collabora in a docker container,
98
+ # which means it will hit itself and not Plone when trying to contact localhost.
99
+ if parts.hostname in ("localhost", "127.0.0.1"):
100
+ self.error_msg = _(
101
+ "error_plone_server_url",
102
+ default=(
103
+ "When Plone is supposed to be accessed on localhost, "
104
+ "Collabora cannot call back. "
105
+ "Use host.docker.internal or a public FQDN instead."
106
+ ),
107
+ )
108
+ logger.error("When Plone runs on localhost, Collabora cannot call back.")
109
+ return ""
110
+ return plone_server_url
111
+
112
+ @property
113
+ @memoize
114
+ def collabora_server_url(self):
115
+ collabora_server_url = api.portal.get_registry_record(
116
+ n(b"collective.collabora.collabora_server_url"), default=None
117
+ )
118
+ if not collabora_server_url:
119
+ self.error_msg = _(
120
+ "error_collabora_server_url_empty",
121
+ default="collective.collabora.collabora_server_url is not configured.",
122
+ )
123
+ logger.error("collective.collabora.collabora_server_url is not configured.")
124
+ if collabora_server_url and not collabora_server_url.startswith("http"):
125
+ self.error_msg = _(
126
+ "error_collabora_server_url_invalid",
127
+ default=(
128
+ "collective.collabora.collabora_server_url "
129
+ "needs to be a full URL."
130
+ ),
131
+ )
132
+ logger.error(
133
+ "collective.collabora.collabora_server_url %s needs to be a full URL",
134
+ collabora_server_url,
135
+ )
136
+ return "" # Don't let server discovery error msg obscure this one
137
+ return collabora_server_url
138
+
139
+ @property
140
+ @memoize
141
+ def server_discovery_xml(self):
142
+ if not self.collabora_server_url:
143
+ return
144
+ try:
145
+ response = requests.get("%s/hosting/discovery" % self.collabora_server_url)
146
+ except requests.exceptions.RequestException as e:
147
+ self.error_msg = error_server_discovery
148
+ logger.error(e)
149
+ return
150
+ if response.status_code == 200:
151
+ return response.text
152
+ self.error_msg = error_server_discovery
153
+ logger.error(
154
+ "Server discovery error %i: %s. Verify your proxy configuration.",
155
+ response.status_code,
156
+ response.reason,
157
+ )
158
+
159
+ @property
160
+ @memoize
161
+ def editor_url(self):
162
+ """Return the URL of the LibreOffice / Collabora editor.
163
+
164
+ - Call wopi_discovery.
165
+ - Get the right URL depending on the file extension from the XML
166
+ """
167
+ if not self.server_discovery_xml:
168
+ return
169
+ parser = etree.XMLParser()
170
+ tree = etree.fromstring(self.server_discovery_xml, parser=parser)
171
+ mime_type = self.stored_file.contentType
172
+ action = tree.xpath("//app[@name='%s']/action" % mime_type)
173
+ action = action[0] if len(action) else None
174
+ if action is None:
175
+ self.error_msg = _(
176
+ "error_editor_mimetype",
177
+ default="Collabora does not support mimetype ${mimetype}.",
178
+ mapping={"mimetype": mime_type},
179
+ )
180
+ logger.error("Collabora does not support mimetype %r.", mime_type)
181
+ return
182
+ urlsrc = action.get("urlsrc")
183
+ if not urlsrc:
184
+ self.error_msg = _(
185
+ "error_editor_urlsrc", default="Cannot extract url source from action"
186
+ )
187
+ logger.error("Cannot extract urlsrc from action %r", action)
188
+ return urlsrc
189
+
190
+ @property
191
+ @memoize
192
+ def jwt_token(self):
193
+ """Return a JWT token from the plone.restapi JWT PAS plugin.
194
+
195
+ This token is used by the WOPI client (LibreOffice / Collabora)
196
+ to authenticate against the WOPI host (Quaive).
197
+ """
198
+ acl_users = api.portal.get_tool("acl_users")
199
+ plugins = acl_users.plugins.listPlugins(IAuthenticationPlugin)
200
+ plugins = filter(
201
+ lambda it: it[1].meta_type == "JWT Authentication Plugin", plugins
202
+ )
203
+ try:
204
+ jwt_plugin = next(plugins)[1]
205
+ except StopIteration:
206
+ self.error_msg = _(
207
+ "error_jwt_plugin",
208
+ default=(
209
+ "JWT Authentication Plugin not found. "
210
+ "Is plone.restapi installed?"
211
+ ),
212
+ )
213
+ logger.error("JWT Authentication Plugin not found.")
214
+ return
215
+ return jwt_plugin.create_token(api.user.get_current().getId())
216
+
217
+ @property
218
+ @memoize
219
+ def wopi_url(self):
220
+ """Return the URL to load the document in LibreOffice / Collabora."""
221
+ if not self.editor_url or not self.jwt_token:
222
+ return
223
+ document_url = self.context.absolute_url()
224
+
225
+ # optionally enable direct backend-to-backend connection
226
+ if self.plone_server_url != self.portal_url:
227
+ document_url = document_url.replace(self.portal_url, self.plone_server_url)
228
+
229
+ uuid = IUUID(self.context)
230
+ args = dict(
231
+ WOPISrc="%s/@@collabora-wopi/files/%s" % (document_url, uuid),
232
+ access_token=self.jwt_token, # login credentials
233
+ # https://sdk.collaboraonline.com/docs/theming.html
234
+ ui_defaults=api.portal.get_registry_record(
235
+ n(b"collective.collabora.ui_defaults"),
236
+ default="UIMode=compact;TextSidebar=false;TextRuler=false;PresentationStatusbar=false;SpreadsheetSidebar=false;", # noqa: E501
237
+ ),
238
+ )
239
+ quoted_args = urlencode(args)
240
+ return "%s%s" % (self.editor_url, quoted_args)
241
+
242
+ @property
243
+ @memoize
244
+ def iframe_is_cors(self):
245
+ """
246
+ Requesting fullscreen works only when CORS protection is not in play,
247
+ i.e. when running Collabora Online via a reverse proxy on the same
248
+ domain and port as Plone itself - that is the recommended way of running
249
+ in production.
250
+ """
251
+ return utils.collabora_is_cors()
252
+
253
+ @property
254
+ def css_import(self):
255
+ return (
256
+ "@import url(%s/++resource++collective.collabora/edit.css" % self.portal_url
257
+ )
@@ -0,0 +1,32 @@
1
+ .section-collabora-actions {
2
+ text-align: right;
3
+ background-color: #d6d6d6;
4
+ margin-bottom: 0;
5
+ padding: 0.5rem;
6
+ }
7
+ .section-collabora-actions.plone6 .btn {
8
+ font-size: 0.9rem;
9
+ }
10
+ .section-collabora-actions.plone4 {
11
+ display: flex;
12
+ justify-content: flex-end;
13
+ }
14
+ #content .section-collabora-actions.plone4 a.btn {
15
+ font-size: 0.9rem;
16
+ margin: 0 0.5rem;
17
+ padding: 0.25rem 0.5rem;
18
+ border-radius: 0.25rem;
19
+ background-color: #6c757d; color:white;
20
+ }
21
+ #content .section-collabora-actions.plone4 a.btn:hover {
22
+ background-color: #555;
23
+ }
24
+ #content .section-collabora-actions.plone4 a.btn.fullscreen {
25
+ margin-right: auto;
26
+ }
27
+ .section-collabora-actions .fullscreen {
28
+ float: left;
29
+ }
30
+ .section-collabora-main iframe {
31
+ border: 0
32
+ }
@@ -0,0 +1,92 @@
1
+ //
2
+ // --- host to collabora ---
3
+ //
4
+
5
+
6
+ function collabora_postMessage(message_id, values={}) {
7
+ var iframe = document.getElementById("cool-iframe");
8
+ var targetOrigin = iframe.getAttribute("collabora_server_url");
9
+ var msg = {
10
+ "MessageId": message_id,
11
+ "SendTime": Date.now(),
12
+ "Values": values
13
+ }
14
+ console.log(msg);
15
+ window.frames[0].postMessage(JSON.stringify(msg), targetOrigin);
16
+ }
17
+
18
+
19
+ function collabora_action_fullscreen() {
20
+ // Requesting fullscreen works only when CORS protection is not in play, i.e.
21
+ // when running Collabora via a reverse proxy on the same domain and port as
22
+ // Plone itself.
23
+ collabora_postMessage("Action_Fullscreen");
24
+ }
25
+
26
+
27
+ function collabora_action_save() {
28
+ collabora_postMessage("Action_Save", {"DontSaveIfUnmodified": "true"});
29
+ }
30
+
31
+ function collabora_action_close() {
32
+ collabora_postMessage("Action_Close");
33
+ }
34
+
35
+ function collabora_action_save_and_close() {
36
+ collabora_action_save();
37
+ collabora_action_close();
38
+ }
39
+
40
+ //
41
+ // --- collabora to host ---
42
+ //
43
+
44
+ function isValidJSON(text) {
45
+ try {
46
+ JSON.parse(text);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function resize_iframe() {
54
+ var iframe = document.getElementById("cool-iframe");
55
+ var plone_version = iframe.getAttribute("plone_version");
56
+
57
+ console.log("Resizing iframe on document loaded");
58
+ if (plone_version == "quaive") {
59
+ var offset = iframe.offsetTop + 80;
60
+ } else if (plone_version == "plone6") {
61
+ var offset = iframe.offsetTop + 5;
62
+ } else if (plone_version == "plone5") {
63
+ var offset = window.document.getElementById("main-container").offsetTop + 55;
64
+ } else {
65
+ var offset = window.document.getElementById("content").offsetTop + 210;
66
+ }
67
+ iframe.style.height = 'calc(100vh - ' + offset + 'px)';
68
+ console.log("Resized cool-iframe");
69
+ }
70
+
71
+ // https://sdk.collaboraonline.com/docs/postmessage_api.html
72
+ function handlePostMessage(e) {
73
+ // The actual message is contained in the data property of the event.
74
+ if (! isValidJSON(e.data)) {
75
+ return;
76
+ }
77
+ var msg = JSON.parse(e.data);
78
+ var msgId = msg.MessageId;
79
+ var msgData = msg.Values;
80
+ console.log('Received message: ' + msgId);
81
+ console.log(msgData);
82
+ if (msgData.Status == 'Frame_Ready') {
83
+ collabora_postMessage("Host_PostmessageReady");
84
+ }
85
+ if (msgData.Status == 'Document_Loaded') {
86
+ resize_iframe();
87
+ }
88
+ }
89
+
90
+ document.addEventListener("DOMContentLoaded", () => {
91
+ window.addEventListener('message', handlePostMessage, false);
92
+ });
@@ -0,0 +1,105 @@
1
+ <html xmlns="http://www.w3.org/1999/xhtml"
2
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
3
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
4
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
5
+ lang="en"
6
+ metal:use-macro="context/main_template/macros/master"
7
+ xml:lang="en"
8
+ i18n:domain="collective.collabora"
9
+ >
10
+ <body>
11
+
12
+ <metal:css fill-slot="style_slot">
13
+ <style type="text/css"
14
+ tal:content="view/css_import"
15
+ >
16
+ </style>
17
+ </metal:css>
18
+ <metal:js fill-slot="javascript_head_slot">
19
+ <script tal:attributes="
20
+ src python: view.portal_url + '/++resource++collective.collabora/edit.js';
21
+ "></script>
22
+ </metal:js>
23
+
24
+ <metal:main fill-slot="main">
25
+ <metal:block define-macro="main"
26
+ tal:define="
27
+ error_msg view/error_msg;
28
+ "
29
+ >
30
+ <div tal:condition="error_msg">
31
+ <h2 i18n:translate="label_error">Error</h2>
32
+ <p class="alert alert-warning"
33
+ tal:content="error_msg"
34
+ >error_msg</p>
35
+ </div>
36
+
37
+ <section tal:condition="not: view/error_msg"
38
+ tal:attributes="
39
+ class python:'section section-collabora-actions ' + view.plone_version;
40
+ "
41
+ >
42
+ <a class="btn btn-secondary standalone fullscreen"
43
+ onclick="collabora_action_fullscreen(); return false;"
44
+ tal:condition="not: view/iframe_is_cors"
45
+ i18n:translate=""
46
+ >Fullscreen</a>
47
+ <a class="btn btn-secondary standalone download"
48
+ tal:attributes="
49
+ href view/download_url;
50
+ "
51
+ >
52
+ <span i18n:translate="">Download</span>
53
+ <span tal:content="python: view.human_readable_size()"
54
+ i18n:ignore=""
55
+ >size</span>
56
+ </a>
57
+ <a class="btn btn-secondary standalone edit"
58
+ tal:condition="view/can_edit"
59
+ tal:attributes="
60
+ href python:context.absolute_url() + '/@@edit';
61
+ "
62
+ i18n:translate=""
63
+ >Edit metadata</a>
64
+ <a class="btn btn-primary context"
65
+ onclick="javascript:collabora_action_save_and_close(); return true;"
66
+ tal:condition="view/can_edit"
67
+ tal:attributes="
68
+ href python:context.absolute_url() + '/view';
69
+ "
70
+ i18n:translate=""
71
+ >Save & Close</a>
72
+ <a class="btn btn-primary context"
73
+ onclick="javascript:collabora_action_close(); return true;"
74
+ tal:condition="not:view/can_edit"
75
+ tal:attributes="
76
+ href python:context.absolute_url() + '/view';
77
+ "
78
+ i18n:translate=""
79
+ >Close</a>
80
+ </section>
81
+
82
+ <section tal:condition="not: view/error_msg"
83
+ tal:attributes="
84
+ class python:'section section-collabora-main ' + view.plone_version;
85
+ "
86
+ >
87
+ <iframe id="cool-iframe"
88
+ allowfullscreen="true"
89
+ height="100%"
90
+ name="cool-iframe"
91
+ scrolling="no"
92
+ width="100%"
93
+ tal:attributes="
94
+ collabora_server_url view/collabora_server_url;
95
+ plone_version view/plone_version;
96
+ src view/wopi_url;
97
+ "
98
+ ></iframe>
99
+ </section>
100
+
101
+ </metal:block>
102
+ </metal:main>
103
+
104
+ </body>
105
+ </html>