Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Iliya Manolov
erp5
Commits
713dbac5
Commit
713dbac5
authored
Jul 04, 2012
by
Łukasz Nowak
Committed by
Rafael Monnerat
Sep 17, 2012
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement Facebook based authentication.
parent
b4d0924a
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
483 additions
and
0 deletions
+483
-0
bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml
...mplateItem/portal_caches/facebook_token_cache_factory.xml
+76
-0
bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml
...es/facebook_token_cache_factory/volatile_cache_plugin.xml
+20
-0
bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml
...kinTemplateItem/portal_skins/erp5_credential_facebook.xml
+26
-0
bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml
...kins/erp5_credential_facebook/Base_createFacebookUser.xml
+67
-0
bt5/erp5_credential_facebook/bt/change_log
bt5/erp5_credential_facebook/bt/change_log
+2
-0
bt5/erp5_credential_facebook/bt/copyright_list
bt5/erp5_credential_facebook/bt/copyright_list
+1
-0
bt5/erp5_credential_facebook/bt/description
bt5/erp5_credential_facebook/bt/description
+1
-0
bt5/erp5_credential_facebook/bt/license
bt5/erp5_credential_facebook/bt/license
+1
-0
bt5/erp5_credential_facebook/bt/revision
bt5/erp5_credential_facebook/bt/revision
+1
-0
bt5/erp5_credential_facebook/bt/template_format_version
bt5/erp5_credential_facebook/bt/template_format_version
+1
-0
bt5/erp5_credential_facebook/bt/template_path_list
bt5/erp5_credential_facebook/bt/template_path_list
+2
-0
bt5/erp5_credential_facebook/bt/template_skin_id_list
bt5/erp5_credential_facebook/bt/template_skin_id_list
+1
-0
bt5/erp5_credential_facebook/bt/title
bt5/erp5_credential_facebook/bt/title
+1
-0
bt5/erp5_credential_facebook/bt/version
bt5/erp5_credential_facebook/bt/version
+1
-0
product/ERP5Security/ERP5FacebookExtractionPlugin.py
product/ERP5Security/ERP5FacebookExtractionPlugin.py
+235
-0
product/ERP5Security/__init__.py
product/ERP5Security/__init__.py
+11
-0
product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt
...rity/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt
+36
-0
No files found.
bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml
0 → 100644
View file @
713dbac5
<?xml version="1.0"?>
<ZopeData>
<record
id=
"1"
aka=
"AAAAAAAAAAE="
>
<pickle>
<global
name=
"Cache Factory"
module=
"erp5.portal_type"
/>
</pickle>
<pickle>
<dictionary>
<item>
<key>
<string>
_count
</string>
</key>
<value>
<persistent>
<string
encoding=
"base64"
>
AAAAAAAAAAI=
</string>
</persistent>
</value>
</item>
<item>
<key>
<string>
_mt_index
</string>
</key>
<value>
<persistent>
<string
encoding=
"base64"
>
AAAAAAAAAAM=
</string>
</persistent>
</value>
</item>
<item>
<key>
<string>
_tree
</string>
</key>
<value>
<persistent>
<string
encoding=
"base64"
>
AAAAAAAAAAQ=
</string>
</persistent>
</value>
</item>
<item>
<key>
<string>
cache_duration
</string>
</key>
<value>
<int>
3600
</int>
</value>
</item>
<item>
<key>
<string>
description
</string>
</key>
<value>
<none/>
</value>
</item>
<item>
<key>
<string>
id
</string>
</key>
<value>
<string>
facebook_token_cache_factory
</string>
</value>
</item>
<item>
<key>
<string>
portal_type
</string>
</key>
<value>
<string>
Cache Factory
</string>
</value>
</item>
<item>
<key>
<string>
title
</string>
</key>
<value>
<none/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record
id=
"2"
aka=
"AAAAAAAAAAI="
>
<pickle>
<global
name=
"Length"
module=
"BTrees.Length"
/>
</pickle>
<pickle>
<int>
0
</int>
</pickle>
</record>
<record
id=
"3"
aka=
"AAAAAAAAAAM="
>
<pickle>
<global
name=
"OOBTree"
module=
"BTrees.OOBTree"
/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record
id=
"4"
aka=
"AAAAAAAAAAQ="
>
<pickle>
<global
name=
"OOBTree"
module=
"BTrees.OOBTree"
/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml
0 → 100644
View file @
713dbac5
<?xml version="1.0"?>
<ZopeData>
<record
id=
"1"
aka=
"AAAAAAAAAAE="
>
<pickle>
<global
name=
"Ram Cache"
module=
"erp5.portal_type"
/>
</pickle>
<pickle>
<dictionary>
<item>
<key>
<string>
id
</string>
</key>
<value>
<string>
volatile_cache_plugin
</string>
</value>
</item>
<item>
<key>
<string>
portal_type
</string>
</key>
<value>
<string>
Ram Cache
</string>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml
0 → 100644
View file @
713dbac5
<?xml version="1.0"?>
<ZopeData>
<record
id=
"1"
aka=
"AAAAAAAAAAE="
>
<pickle>
<global
name=
"Folder"
module=
"OFS.Folder"
/>
</pickle>
<pickle>
<dictionary>
<item>
<key>
<string>
_objects
</string>
</key>
<value>
<tuple/>
</value>
</item>
<item>
<key>
<string>
id
</string>
</key>
<value>
<string>
erp5_credential_facebook
</string>
</value>
</item>
<item>
<key>
<string>
title
</string>
</key>
<value>
<string></string>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml
0 → 100644
View file @
713dbac5
<?xml version="1.0"?>
<ZopeData>
<record
id=
"1"
aka=
"AAAAAAAAAAE="
>
<pickle>
<global
name=
"PythonScript"
module=
"Products.PythonScripts.PythonScript"
/>
</pickle>
<pickle>
<dictionary>
<item>
<key>
<string>
Script_magic
</string>
</key>
<value>
<int>
3
</int>
</value>
</item>
<item>
<key>
<string>
_bind_names
</string>
</key>
<value>
<object>
<klass>
<global
name=
"NameAssignments"
module=
"Shared.DC.Scripts.Bindings"
/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key>
<string>
_asgns
</string>
</key>
<value>
<dictionary>
<item>
<key>
<string>
name_container
</string>
</key>
<value>
<string>
container
</string>
</value>
</item>
<item>
<key>
<string>
name_context
</string>
</key>
<value>
<string>
context
</string>
</value>
</item>
<item>
<key>
<string>
name_m_self
</string>
</key>
<value>
<string>
script
</string>
</value>
</item>
<item>
<key>
<string>
name_subpath
</string>
</key>
<value>
<string>
traverse_subpath
</string>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key>
<string>
_body
</string>
</key>
<value>
<string>
raise NotImplementedError(\'This script shall be overlodad, as user creation is project specific\')\n
</string>
</value>
</item>
<item>
<key>
<string>
_params
</string>
</key>
<value>
<string>
tag, first_name, last_name, reference, email
</string>
</value>
</item>
<item>
<key>
<string>
id
</string>
</key>
<value>
<string>
Base_createFacebookUser
</string>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
bt5/erp5_credential_facebook/bt/change_log
0 → 100644
View file @
713dbac5
2012/07/04 Łukasz Nowak
* Initial version
\ No newline at end of file
bt5/erp5_credential_facebook/bt/copyright_list
0 → 100644
View file @
713dbac5
Nexedi
\ No newline at end of file
bt5/erp5_credential_facebook/bt/description
0 → 100644
View file @
713dbac5
Facebook based credential system.
\ No newline at end of file
bt5/erp5_credential_facebook/bt/license
0 → 100644
View file @
713dbac5
GPL
\ No newline at end of file
bt5/erp5_credential_facebook/bt/revision
0 → 100644
View file @
713dbac5
1
\ No newline at end of file
bt5/erp5_credential_facebook/bt/template_format_version
0 → 100644
View file @
713dbac5
1
\ No newline at end of file
bt5/erp5_credential_facebook/bt/template_path_list
0 → 100644
View file @
713dbac5
portal_caches/facebook_token_cache_factory
portal_caches/facebook_token_cache_factory/volatile_cache_plugin
\ No newline at end of file
bt5/erp5_credential_facebook/bt/template_skin_id_list
0 → 100644
View file @
713dbac5
erp5_credential_facebook
\ No newline at end of file
bt5/erp5_credential_facebook/bt/title
0 → 100644
View file @
713dbac5
erp5_credential_facebook
\ No newline at end of file
bt5/erp5_credential_facebook/bt/version
0 → 100644
View file @
713dbac5
0.1
\ No newline at end of file
product/ERP5Security/ERP5FacebookExtractionPlugin.py
0 → 100644
View file @
713dbac5
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2012 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly advised to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from
Products.ERP5Type.Globals
import
InitializeClass
from
AccessControl
import
ClassSecurityInfo
from
Products.PageTemplates.PageTemplateFile
import
PageTemplateFile
from
Products.PluggableAuthService.interfaces
import
plugins
from
Products.PluggableAuthService.utils
import
classImplements
from
Products.PluggableAuthService.plugins.BasePlugin
import
BasePlugin
from
Products.ERP5Security.ERP5UserManager
import
SUPER_USER
from
Products.PluggableAuthService.PluggableAuthService
import
DumbHTTPExtractor
from
AccessControl.SecurityManagement
import
getSecurityManager
,
\
setSecurityManager
,
newSecurityManager
from
Products.ERP5Type.Cache
import
DEFAULT_CACHE_SCOPE
import
socket
from
Products.ERP5Security.ERP5UserManager
import
getUserByLogin
from
zLOG
import
LOG
,
ERROR
,
INFO
try
:
import
facebook
except
ImportError
:
facebook
=
None
#Form for new plugin in ZMI
manage_addERP5FacebookExtractionPluginForm
=
PageTemplateFile
(
'www/ERP5Security_addERP5FacebookExtractionPlugin'
,
globals
(),
__name__
=
'manage_addERP5FacebookExtractionPluginForm'
)
def
addERP5FacebookExtractionPlugin
(
dispatcher
,
id
,
title
=
None
,
REQUEST
=
None
):
""" Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """
plugin
=
ERP5FacebookExtractionPlugin
(
id
,
title
)
dispatcher
.
_setObject
(
plugin
.
getId
(),
plugin
)
if
REQUEST
is
not
None
:
REQUEST
[
'RESPONSE'
].
redirect
(
'%s/manage_workspace'
'?manage_tabs_message='
'ERP5FacebookExtractionPlugin+added.'
%
dispatcher
.
absolute_url
())
class
ERP5FacebookExtractionPlugin
(
BasePlugin
):
"""
Plugin to authenicate as machines.
"""
meta_type
=
"ERP5 Facebook Extraction Plugin"
# cache_fatory_name proposal to begin configurable
cache_factory_name
=
'facebook_token_cache_factory'
reference_prefix
=
'fb_'
security
=
ClassSecurityInfo
()
def
__init__
(
self
,
id
,
title
=
None
):
#Register value
self
.
_setId
(
id
)
self
.
title
=
title
#####################
# memcached helpers #
#####################
def
_getCacheFactory
(
self
):
portal
=
self
.
getPortalObject
()
cache_tool
=
portal
.
portal_caches
cache_factory
=
cache_tool
.
getRamCacheRoot
().
get
(
self
.
cache_factory_name
)
#XXX This conditional statement should be remove as soon as
#Broadcasting will be enable among all zeo clients.
#Interaction which update portal_caches should interact with all nodes.
if
cache_factory
is
None
\
and
getattr
(
cache_tool
,
self
.
cache_factory_name
,
None
)
is
not
None
:
#ram_cache_root is not up to date for current node
cache_tool
.
updateCache
()
cache_factory
=
cache_tool
.
getRamCacheRoot
().
get
(
self
.
cache_factory_name
)
if
cache_factory
is
None
:
raise
KeyError
return
cache_factory
def
setFacebookToken
(
self
,
key
,
body
):
cache_factory
=
self
.
_getCacheFactory
()
cache_duration
=
cache_factory
.
cache_duration
for
cache_plugin
in
cache_factory
.
getCachePluginList
():
cache_plugin
.
set
(
key
,
DEFAULT_CACHE_SCOPE
,
body
,
cache_duration
=
cache_duration
)
def
getFacebookToken
(
self
,
key
):
cache_factory
=
self
.
_getCacheFactory
()
for
cache_plugin
in
cache_factory
.
getCachePluginList
():
cache_entry
=
cache_plugin
.
get
(
key
,
DEFAULT_CACHE_SCOPE
)
if
cache_entry
is
not
None
:
return
cache_entry
.
getValue
()
raise
KeyError
(
'Key %r not found'
%
key
)
def
getFacebookEntry
(
self
,
token
):
timeout
=
socket
.
getdefaulttimeout
()
try
:
# require really fast interaction
socket
.
setdefaulttimeout
(
5
)
facebook_entry
=
facebook
.
GraphAPI
(
token
).
get_object
(
"me"
)
except
Exception
:
facebook_entry
=
None
finally
:
socket
.
setdefaulttimeout
(
timeout
)
user_entry
=
{}
if
facebook_entry
is
not
None
:
# sanitise value
try
:
for
k
in
(
'first_name'
,
'last_name'
,
'id'
,
'email'
):
if
k
==
'id'
:
user_entry
[
'reference'
]
=
self
.
reference_prefix
+
facebook_entry
[
k
].
encode
(
'utf-8'
)
else
:
user_entry
[
k
]
=
facebook_entry
[
k
].
encode
(
'utf-8'
)
except
KeyError
:
user_entry
=
None
return
user_entry
####################################
#ILoginPasswordHostExtractionPlugin#
####################################
security
.
declarePrivate
(
'extractCredentials'
)
def
extractCredentials
(
self
,
request
):
""" Extract facebook credentials from the request header. """
Base_createFacebookUser
=
getattr
(
self
.
getPortalObject
(),
'Base_createFacebookUser'
,
None
)
if
facebook
is
None
or
Base_createFacebookUser
is
None
:
# no facebook library available or not configured
if
facebook
is
None
:
LOG
(
'ERP5FacebookExtractionPlugin'
,
INFO
,
'No facebook module available, disabled authentication.'
)
if
Base_createFacebookUser
is
None
:
LOG
(
'ERP5FacebookExtractionPlugin'
,
INFO
,
'No Base_createFacebookUser script available, install '
'erp5_credential_facebook, disabled authentication.'
)
return
DumbHTTPExtractor
().
extractCredentials
(
request
)
creds
=
{}
token
=
None
if
request
.
_auth
is
not
None
:
# 1st - try to fetch from Authorization header
if
'facebook'
in
request
.
_auth
.
lower
():
l
=
request
.
_auth
.
split
()
if
len
(
l
)
==
2
:
token
=
l
[
1
]
if
token
is
None
:
# no token
return
DumbHTTPExtractor
().
extractCredentials
(
request
)
# token is available
user
=
None
facebook_entry
=
None
try
:
user
=
self
.
getFacebookToken
(
token
)
except
KeyError
:
facebook_entry
=
self
.
getFacebookEntry
(
token
)
if
facebook_entry
is
not
None
:
user
=
facebook_entry
[
'reference'
]
if
user
is
None
:
# fallback to default way
return
DumbHTTPExtractor
().
extractCredentials
(
request
)
tag
=
'%s_user_creation_in_progress'
%
user
if
self
.
getPortalObject
().
portal_activities
.
countMessageWithTag
(
tag
)
>
0
:
self
.
REQUEST
[
'USER_CREATION_IN_PROGRESS'
]
=
user
else
:
# create the user if not found
person_list
=
getUserByLogin
(
self
.
getPortalObject
(),
user
)
if
len
(
person_list
)
==
0
:
sm
=
getSecurityManager
()
if
sm
.
getUser
().
getId
()
!=
SUPER_USER
:
newSecurityManager
(
self
,
self
.
getUser
(
SUPER_USER
))
try
:
self
.
REQUEST
[
'USER_CREATION_IN_PROGRESS'
]
=
user
if
facebook_entry
is
None
:
facebook_entry
=
self
.
getFacebookEntry
(
token
)
try
:
self
.
Base_createFacebookUser
(
tag
,
**
facebook_entry
)
except
Exception
:
LOG
(
'ERP5FacebookExtractionPlugin'
,
ERROR
,
'Issue while calling creation script:'
,
error
=
True
)
raise
finally
:
setSecurityManager
(
sm
)
try
:
self
.
setFacebookToken
(
token
,
user
)
except
KeyError
:
# allow to work w/o cache
pass
creds
[
'external_login'
]
=
user
creds
[
'remote_host'
]
=
request
.
get
(
'REMOTE_HOST'
,
''
)
try
:
creds
[
'remote_address'
]
=
request
.
getClientAddr
()
except
AttributeError
:
creds
[
'remote_address'
]
=
request
.
get
(
'REMOTE_ADDR'
,
''
)
return
creds
manage_editERP5FacebookExtractionPluginForm
=
PageTemplateFile
(
'www/ERP5Security_editERP5FacebookExtractionPlugin'
,
globals
(),
__name__
=
'manage_editERP5FacebookExtractionPluginForm'
)
#List implementation of class
classImplements
(
ERP5FacebookExtractionPlugin
,
plugins
.
ILoginPasswordHostExtractionPlugin
)
InitializeClass
(
ERP5FacebookExtractionPlugin
)
product/ERP5Security/__init__.py
View file @
713dbac5
...
@@ -28,6 +28,7 @@ import ERP5UserFactory
...
@@ -28,6 +28,7 @@ import ERP5UserFactory
import
ERP5KeyAuthPlugin
import
ERP5KeyAuthPlugin
import
ERP5ExternalAuthenticationPlugin
import
ERP5ExternalAuthenticationPlugin
import
ERP5BearerExtractionPlugin
import
ERP5BearerExtractionPlugin
import
ERP5FacebookExtractionPlugin
def
mergedLocalRoles
(
object
):
def
mergedLocalRoles
(
object
):
"""Returns a merging of object and its ancestors'
"""Returns a merging of object and its ancestors'
...
@@ -64,6 +65,7 @@ registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type)
...
@@ -64,6 +65,7 @@ registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type)
registerMultiPlugin
(
ERP5KeyAuthPlugin
.
ERP5KeyAuthPlugin
.
meta_type
)
registerMultiPlugin
(
ERP5KeyAuthPlugin
.
ERP5KeyAuthPlugin
.
meta_type
)
registerMultiPlugin
(
ERP5ExternalAuthenticationPlugin
.
ERP5ExternalAuthenticationPlugin
.
meta_type
)
registerMultiPlugin
(
ERP5ExternalAuthenticationPlugin
.
ERP5ExternalAuthenticationPlugin
.
meta_type
)
registerMultiPlugin
(
ERP5BearerExtractionPlugin
.
ERP5BearerExtractionPlugin
.
meta_type
)
registerMultiPlugin
(
ERP5BearerExtractionPlugin
.
ERP5BearerExtractionPlugin
.
meta_type
)
registerMultiPlugin
(
ERP5FacebookExtractionPlugin
.
ERP5FacebookExtractionPlugin
.
meta_type
)
def
initialize
(
context
):
def
initialize
(
context
):
...
@@ -130,6 +132,15 @@ def initialize(context):
...
@@ -130,6 +132,15 @@ def initialize(context):
,
icon
=
'www/portal.gif'
,
icon
=
'www/portal.gif'
)
)
context
.
registerClass
(
ERP5FacebookExtractionPlugin
.
ERP5FacebookExtractionPlugin
,
permission
=
ManageUsers
,
constructors
=
(
ERP5FacebookExtractionPlugin
.
manage_addERP5FacebookExtractionPluginForm
,
ERP5FacebookExtractionPlugin
.
addERP5FacebookExtractionPlugin
,
)
,
visibility
=
None
,
icon
=
'www/portal.gif'
)
from
AccessControl.SecurityInfo
import
ModuleSecurityInfo
from
AccessControl.SecurityInfo
import
ModuleSecurityInfo
ModuleSecurityInfo
(
'Products.ERP5Security.ERP5UserManager'
).
declarePublic
(
ModuleSecurityInfo
(
'Products.ERP5Security.ERP5UserManager'
).
declarePublic
(
'getUserByLogin'
)
'getUserByLogin'
)
product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt
0 → 100644
View file @
713dbac5
<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
<h2 tal:define="form_title string:Add ERP5 Facebook Extraction Plugin"
tal:replace="structure context/manage_form_title">FORM TITLE</h2>
<p class="form-help">Please input the configuration</p>
<form action="addERP5FacebookExtractionPlugin" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td colspan="2"> <input type="submit" value="add plugin"/>
</td>
</tr>
</table>
</form>
<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment