From 37265511172a0afe89a585fd4055e24e25a2fa52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Nowak?= <luke@nexedi.com>
Date: Wed, 4 Jul 2012 15:35:52 +0200
Subject: [PATCH] Implement Facebook based authentication.

---
 .../facebook_token_cache_factory.xml          |  76 ++++++
 .../volatile_cache_plugin.xml                 |  20 ++
 .../portal_skins/erp5_credential_facebook.xml |  26 ++
 .../Base_createFacebookUser.xml               |  67 +++++
 bt5/erp5_credential_facebook/bt/change_log    |   2 +
 .../bt/copyright_list                         |   1 +
 bt5/erp5_credential_facebook/bt/description   |   1 +
 bt5/erp5_credential_facebook/bt/license       |   1 +
 bt5/erp5_credential_facebook/bt/revision      |   1 +
 .../bt/template_format_version                |   1 +
 .../bt/template_path_list                     |   2 +
 .../bt/template_skin_id_list                  |   1 +
 bt5/erp5_credential_facebook/bt/title         |   1 +
 bt5/erp5_credential_facebook/bt/version       |   1 +
 .../ERP5FacebookExtractionPlugin.py           | 235 ++++++++++++++++++
 product/ERP5Security/__init__.py              |  11 +
 ...curity_addERP5FacebookExtractionPlugin.zpt |  36 +++
 17 files changed, 483 insertions(+)
 create mode 100644 bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml
 create mode 100644 bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml
 create mode 100644 bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml
 create mode 100644 bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml
 create mode 100644 bt5/erp5_credential_facebook/bt/change_log
 create mode 100644 bt5/erp5_credential_facebook/bt/copyright_list
 create mode 100644 bt5/erp5_credential_facebook/bt/description
 create mode 100644 bt5/erp5_credential_facebook/bt/license
 create mode 100644 bt5/erp5_credential_facebook/bt/revision
 create mode 100644 bt5/erp5_credential_facebook/bt/template_format_version
 create mode 100644 bt5/erp5_credential_facebook/bt/template_path_list
 create mode 100644 bt5/erp5_credential_facebook/bt/template_skin_id_list
 create mode 100644 bt5/erp5_credential_facebook/bt/title
 create mode 100644 bt5/erp5_credential_facebook/bt/version
 create mode 100644 product/ERP5Security/ERP5FacebookExtractionPlugin.py
 create mode 100644 product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt

diff --git a/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml
new file mode 100644
index 0000000000..6a946ca13f
--- /dev/null
+++ b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml
@@ -0,0 +1,76 @@
+<?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>
diff --git a/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml
new file mode 100644
index 0000000000..3455556ba0
--- /dev/null
+++ b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml
@@ -0,0 +1,20 @@
+<?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>
diff --git a/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml
new file mode 100644
index 0000000000..dc3f5fcad5
--- /dev/null
+++ b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml
@@ -0,0 +1,26 @@
+<?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>
diff --git a/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml
new file mode 100644
index 0000000000..8bc75f0afa
--- /dev/null
+++ b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml
@@ -0,0 +1,67 @@
+<?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>
diff --git a/bt5/erp5_credential_facebook/bt/change_log b/bt5/erp5_credential_facebook/bt/change_log
new file mode 100644
index 0000000000..055bbce55f
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/change_log
@@ -0,0 +1,2 @@
+2012/07/04 Łukasz Nowak
+* Initial version
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/copyright_list b/bt5/erp5_credential_facebook/bt/copyright_list
new file mode 100644
index 0000000000..8dfa82e635
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/copyright_list
@@ -0,0 +1 @@
+Nexedi
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/description b/bt5/erp5_credential_facebook/bt/description
new file mode 100644
index 0000000000..039c4845f2
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/description
@@ -0,0 +1 @@
+Facebook based credential system.
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/license b/bt5/erp5_credential_facebook/bt/license
new file mode 100644
index 0000000000..3a3e12bcad
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/license
@@ -0,0 +1 @@
+GPL
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/revision b/bt5/erp5_credential_facebook/bt/revision
new file mode 100644
index 0000000000..56a6051ca2
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/revision
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/template_format_version b/bt5/erp5_credential_facebook/bt/template_format_version
new file mode 100644
index 0000000000..56a6051ca2
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/template_format_version
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/template_path_list b/bt5/erp5_credential_facebook/bt/template_path_list
new file mode 100644
index 0000000000..6091d5ff7a
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/template_path_list
@@ -0,0 +1,2 @@
+portal_caches/facebook_token_cache_factory
+portal_caches/facebook_token_cache_factory/volatile_cache_plugin
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/template_skin_id_list b/bt5/erp5_credential_facebook/bt/template_skin_id_list
new file mode 100644
index 0000000000..09ca68d954
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/template_skin_id_list
@@ -0,0 +1 @@
+erp5_credential_facebook
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/title b/bt5/erp5_credential_facebook/bt/title
new file mode 100644
index 0000000000..09ca68d954
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/title
@@ -0,0 +1 @@
+erp5_credential_facebook
\ No newline at end of file
diff --git a/bt5/erp5_credential_facebook/bt/version b/bt5/erp5_credential_facebook/bt/version
new file mode 100644
index 0000000000..ceab6e11ec
--- /dev/null
+++ b/bt5/erp5_credential_facebook/bt/version
@@ -0,0 +1 @@
+0.1
\ No newline at end of file
diff --git a/product/ERP5Security/ERP5FacebookExtractionPlugin.py b/product/ERP5Security/ERP5FacebookExtractionPlugin.py
new file mode 100644
index 0000000000..56b39e671d
--- /dev/null
+++ b/product/ERP5Security/ERP5FacebookExtractionPlugin.py
@@ -0,0 +1,235 @@
+# -*- 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'
+
+    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)
+
diff --git a/product/ERP5Security/__init__.py b/product/ERP5Security/__init__.py
index ea30b2e7dd..efde32ddf0 100644
--- a/product/ERP5Security/__init__.py
+++ b/product/ERP5Security/__init__.py
@@ -28,6 +28,7 @@ import ERP5UserFactory
 import ERP5KeyAuthPlugin
 import ERP5ExternalAuthenticationPlugin
 import ERP5BearerExtractionPlugin
+import ERP5FacebookExtractionPlugin
 
 def mergedLocalRoles(object):
   """Returns a merging of object and its ancestors'
@@ -64,6 +65,7 @@ registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type)
 registerMultiPlugin(ERP5KeyAuthPlugin.ERP5KeyAuthPlugin.meta_type)
 registerMultiPlugin(ERP5ExternalAuthenticationPlugin.ERP5ExternalAuthenticationPlugin.meta_type)
 registerMultiPlugin(ERP5BearerExtractionPlugin.ERP5BearerExtractionPlugin.meta_type)
+registerMultiPlugin(ERP5FacebookExtractionPlugin.ERP5FacebookExtractionPlugin.meta_type)
 
 def initialize(context):
 
@@ -130,6 +132,15 @@ def initialize(context):
                          , 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
 ModuleSecurityInfo('Products.ERP5Security.ERP5UserManager').declarePublic(
   'getUserByLogin')
diff --git a/product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt b/product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt
new file mode 100644
index 0000000000..4fb943e6b3
--- /dev/null
+++ b/product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt
@@ -0,0 +1,36 @@
+<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>
-- 
2.30.9