--- /dev/null
+A Zope Product that manages Groups of Users
--- /dev/null
+v3.55.1 - 2007-11-08
+
+ * Fix #6984: Now GroupData verifies if it is related to GRUF group or PlonePAS
+ group.
+ [encolpe]
+
+v3.54.4 - 2007-04-29
+
+ * Death to tabindexes!
+ [limi]
+
+v3.54.3 - 2007-04-16
+
+ * Update methods to provide protection against XSS attacks via GET requests
+ [bloodbare, alecm]
+
+v3.54.2 - 2007-02-06
+
+ * Fix a bug in group removing in another group.
+ [encolpe]
+
+v3.54.1 - 2006-12-15
+
+ * Fix _getMemberIds for LDAPUserFolder 2.7 when groups are stored in LDAPUF
+ [encolpe]
+
+ * Got rid of zLOG in favor of logging.
+ [stefan]
+
+v3.54 - 2006-09-19
+ * Fix a bug with LDAPUserFolder where another UserFolder was returned when LUF
+ was requested [jvloothuis]
+
+v3.53 - 2006-09-08
+ * Removed refresh.txt. You should add this locally if you want to use it.
+ [hannosch]
+
+ * getUsers: efficiency improvement: anti-double user inclusion is done by
+ checking key presence in a dict instead of looking up name in a list
+ [b_mathieu]
+
+ * Fix searchUsersByAttribute returning twice the same user id when a second
+ source is present
+ [b_mathieu]
+
+v3.52 - 2006-05-30
+
+ * Plone 2.1 service release
+
+v3.51 - 2006-05-15
+
+ * Changed getLocalRolesForDisplay to check for 'Manage properties' instead of
+ 'Access contents information'. This is still not perfect but at least
+ Anonymous can no longer get at local roles information this way.
+ Fixes http://dev.plone.org/plone/ticket/5492
+ [stefan]
+
+ * Remove some noise log message and the product name parameter from ToolInit.
+ [hannosch]
+
+ * Forces exact match with LDAP on user search
+
+v3.5 - 2005-12-20
+
+ * By default, uses title instead of meta_type in the source management
+ pages. [pjgrizel]
+
+ * It's now possible to search very quickly users from a group
+ in LDAP; long-awaited improvement! [pjgrizel]
+
+ * Correct some wrong security settings.
+ [hannosch]
+
+ * Fix some stupid failing tests so finally all tests pass again.
+ [hannosch]
+
+ * Fix encoding warning in GroupUserFolder.py
+ [encolpe]
+
+ * Made the GroupDataTool call notifyModified() on members who are
+ added or removed from a group
+ [bmh]
+
+v3.4 - 20050904
+
+ * Dynamically fixed the remaining bug in folder_localrole_form.
+
+ * Now the users tab in ZMI allow you to search a user (useful w/ LDAP)
+
+ * Fixed a bug in Plone 2.0 UI when searching a large set of users
+
+ * Added a 'wizard' tab to help in managing LDAP sources.
+
+ * Fixed getProperty in GroupDataTool not to acquire properties.
+ [panjunyong]
+
+[v3.3 - 20050725]
+
+ * Added an 'enable/disable' feature on the sources. Now, you can entierly
+ disable a users source without actually removing it. Very useful for
+ testing purposes!
+
+ * Removed an optimization when user is None in authenticate(), so
+ than you can use GRUF with CASUserFolder (thanks to Alexandre
+ Sauv?mr.lex@free.fr>)
+
+ * Fixed 1235351 (possible infinite recursion in an audit method)
+
+ * Fixed [ 1243323 ] GRUF: bug in createGrouparea() in GroupsTool.py
+
+[v3.23 - 20050724]
+
+ * Fixed unit tests. Now the unit tests are working with the latest ZTC
+ version.
+ [tiran]
+
+[v3.22 - 20050706]
+
+ * Fixed a missing import in GroupsTool.py (http://plone.org/collector/4209)
+ [hannosch]
+
+ * Fixed a nested groups issue with LDAPUserFolder. This is not a widely
+ used schema with ldap anyway.
+ [pjgrizel]
+
+ * Fixed LDAPUserFolderAdapter's search_by_dn bug: search by _login_attr
+ but not _rdnattr
+ [panjunyong]
+
+ * _getLocalRolesForDisplay was marking users as groups for groups that had
+ the same as users (http://plone.org/collector/3711). Made unit tests run
+ even if LDAPUserFolder is not installed.
+ [alecm]
+
+[v3.2 - 20050307]
+
+ Service release.
+
+[v3.2RC2 - 20050305]
+
+ * Now your user sources (especially LUF) can have a 'portait' property which
+ will be used as your user's portrait. This works only in 'preview.txt'-mode.
+
+ * You can put a 'notifyGroupAreaCreated' in your 'groups' folder as you would
+ be able to put a 'notifyMemberAreaCreated' in your 'members' folder.
+ So you can execute some code at group area creation. Thanks to F. Carlier !
+
+ * Added a helper table on the sources tab to help managing LUF group mappings
+
+ * Fixed a bug in Zope 2.7 preventing the zope quickstart page to show up.
+ A hasUsers() method was missing from GRUF's API.
+
+ * Fixed a bug in ZMI which prevented LUF cached users to be individually
+ managed by GRUF.
+
+
+[v3.2RC1 - 20041215]
+
+ * _doChangeUser and _doChangeGroup lost existing groups if the groups argument
+ was omitted. Fixed these and the Zope 2.5-style APIs accordingly.
+ [stefan]
+
+ * Updated API to have a better conformance to the original Zope API.
+ Thanks to Stefan H Holek for this clever advice.
+
+ * Uncommented cache clearing code in _doChangeUser as it appears to be required.
+ [stefan]
+
+ * Added a Plone 2.0 optional patch to improve LDAP and groups management.
+ It's basically a preview of what will be done in Plone 2.1 for users managment.
+ For example, now, you can assign local roles to users in your LDAP directory,
+ EVEN if they're not in the cache in folder_localrole_form.
+ Other "preview" features will come later. Please read README and PloneFeaturePreview.py
+ files for more explanations on these.
+
+ * Made manage_GRUFUsers page a little faster with LDAP by preventing users count.
+
+ * Fixed [ 1051387 ] addGroup fails if type 'Folder' is not implicitly addable.
+
+ * Fixed other minor or cosmetic bugs
+
+ * Group mapping is automatically created by LDAPGroupFolder when you create a group
+ with its interface.
+
+v3_1_1 - 20040906
+
+ * Fixed a bug in getProperty() - it always returned None !
+
+ * Fixed a bug which caused AUTHENTICATED_USER source id to be invalid
+
+v3_1 - 20040831
+
+ * Group-to-role mapping now works for LDAPGroupFolder
+
+ * Debug mode now allows broken source not to be checken against
+
+ * Fixed getUser() bug with remote_user_mode (getUser(None) now returns None).
+ Thanks to Marco Bizzari.
+
+v3_0 - 20040623
+
+ * Minor interface changes
+
+ * Documentation update
+
+v3_0Beta2
+
+ * Various bug fixes
+
+ * Better support for Plone UI. PLEASE USE PLONE2's pjgrizel-gruf3-branch IN SVN!
+ See README-Plone for further explanation
+
+v3_0Beta1
+
+ * API REFACTORING
+
+ * FAR BETTER LDAP SUPPORT (see README-LDAP.stx)
+
+v2_0 - 20040302
+
+ * Reindexing new GroupSpace objects
+ 2004/03/10 Maik Rder
+
+ * Speedup improvements by Heldge Tesdal
+
+ * Fixed ZMI overview refreshing bug
+
+ * GroupsTool method createGrouparea now calls the GroupSpace
+ method setInitialGroup with the group that it is created for.
+ In case this method does not exists, the default behaviour
+ is employed. This is done so that the GroupSpace can decide on its
+ own what the policy should be regarding the group that it is
+ initially created for.
+ See the implementation of GrufSpaces for an example of how this
+ can be used in order to give the initial group the role GroupMember.
+ 2004/02/25 Maik Rder
+
+ * Removed GroupSpace code, which can now be found in
+ http://ingeniweb.sourceforge.net/Products/GrufSpaces
+ 2004/02/25 Maik Rder
+
+v2_0Beta3 - 20040224
+
+ * Improved performance on LDAP Directories
+
+ * Fixed various Plone UI bugs (password & roles changing)
+
+ * Fixed "AttributeError: URL1" bug in ZMI
+
+v2_0Beta2 - 20031222
+
+ * Added GroupSpace object for Plone websites (see website/GroupSpaceDesign_xx.stx)
+
+ * Fixed __getattr__-related bug
+
+ * Fixed inituser-related bug
+
+ * Cosmetic fixes and minor bugs
+
+v2_0Beta1 - 20031026
+
+ * Include support for multi-sources
+
+v1_32 - 20030923
+
+ * Pass __getitem__ access onto user objects (XUF compatibility)
+
+ * Allow ZMI configuration of group workspaces (CMF Tool)
+
+ * Added security declarations to CMF tools
+
+ * new getPureUserNames() and getPurseUsers() methods to get user
+ objects without group objects
+
+v1_31 - 20030731
+
+ * Many performance improvements (tree and audit views)
+
+ * Fixed a recursion pb. on the left pane tree (!)
+
+ * Added a batch view for "overview" page when there's more than
+ 100 users registered in the system
+
+v1_3 - 20030723
+
+ * GRUF NOW SUPPORTS NESTED GROUPS - Transparently, of course.
+
+ * Updated website information & screenshots
+
+ * Major ZMI improving, including everywhere-to-everywhere links,
+ edition of a single user or group, and minor cosmetic fixes
+
+ * The tree view in ZMI now show groups and user (if there's no more
+ than 50, to avoid having server outage)
+
+ * Improved performance
+
+ * Improved test plan
+
+ * Fixed a bug in password generation algorythm with non-iso Python installs
+
+ * Fixed a minor bug in group acquisition stack (it apparently had no side-effect)
+
+v1_21 - 20030710
+
+ * ZMI cosmetic fixes
+
+ * Fixed the bug that prevented LDAP-defined attributes to be acquired by GRUFUser.
+ This bug showed-up with LDAPUserFolder.
+
+v1_2 - 20030709
+
+ * HTML documentation
+
+ * Add a management tab on GRUF to allow users and groups to be created
+ at this top-level management interface.
+
+v1_1 - 20030702
+
+ * Security improvements
+
+ * Added an 'audit' tab to check what's going on
+
+ * GroupsTool and GroupDataTool added for Plone
+
+ * Improved Plone skins
+
+ * Improved Plone installation
+
+ * GRUF Users now 'inherit' from their underlying user object
+
+v1_0RC1 - 20030514
+
+ * Code cleaning
+
+ * Documentation improving
+
+ * API improving (added a few utility methods)
+
+ * UI improving (see skins changes)
+
+ * getId() bug fixing (see ChangeLog)
+
+v0_2 - 20030331
+
+ * Users are now acquired correctly, which prevents you from hotfixing anything !!! :-)
+
+ * This fixed Zope 2.5 w/ Plone bug
+
+ * Better log reporting
+
+ * Validated with LDAPUserFolder and SimpleUserFolder
+
+v0_1 - 20021126
+
+ * User creation is now supported
+
+ * Fixed a bug (with an axe) that prevented Zope module Owner.py code to work.
+ The Owner.py calls aq_inner and aq_parent methods on a User object to get its
+ security context. So it found the underlying User object instead of the GRUF
+ itself. So we fixed this by setting dummy UserFolder-context methods on the
+ GRUFUser objects. This is ugly and should be fixed later by acquiring the
+ underlying User object from a better context.
+
+ * Fixed getUserName in GRUFUser that returned group names without the "group"
+ prefix.
+
+ * Fixed various "cosmetic" bugs
+
+ * Documented the whole stuff
+
+v0_0 - 20021126
+
+ Started to work on this wonderful product.
+
--- /dev/null
+
+CONTRIBUTORS
+
+ P.-J. Grizel <grizel@ingeniweb.com> : Lead programming, Design, Coding, Testing, ZMI interfaces
+
+ O. Deckmyn <deckmyn@ingeniweb.com>: Design, Main user interface
+
+ J Cameron Cooper <jccooper@jcameroncooper.com>: GroupDataTool and GroupsTool design & coding
+
+ Brent Hendricks <brentmh@ece.rice.edu>: GroupDataTool and GroupsTool design & coding
+
+ Helge Tesdal <info@plonesolutions.com> merged PJ's multi-groups branch to the 1.32 GRUF version.
+
+ Maik Röder <maik.roeder@ingeniweb.com>: moved GroupSpace out of GRUF
+
+ Volker: LDAPGroupFolder initial coding
+
+ Kai Bielenberg <kai@bielenberg.info>: LDAP tips
+
+ Jens Vagelpohl <jens@dataflake.org>: Help on LDAPUserFolder support
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+DynaList.py => a list that has dynamic data (ie. calculated by a 'data' method).
+Please override this class and define a data(self,) method that will return the actual list.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: DynaList.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+class DynaList:
+ def __init__(self, initlist=None):
+ pass
+
+ def __repr__(self): return repr(self.data())
+ def __lt__(self, other): return self.data() < self.__cast(other)
+ def __le__(self, other): return self.data() <= self.__cast(other)
+ def __eq__(self, other): return self.data() == self.__cast(other)
+ def __ne__(self, other): return self.data() != self.__cast(other)
+ def __gt__(self, other): return self.data() > self.__cast(other)
+ def __ge__(self, other): return self.data() >= self.__cast(other)
+ def __cast(self, other):
+ if isinstance(other, UserList): return other.data()
+ else: return other
+ def __cmp__(self, other):
+ raise RuntimeError, "UserList.__cmp__() is obsolete"
+ def __contains__(self, item): return item in self.data()
+ def __len__(self): return len(self.data())
+ def __getitem__(self, i): return self.data()[i]
+ def __setitem__(self, i, item): self.data()[i] = item
+ def __delitem__(self, i): del self.data()[i]
+ def __getslice__(self, i, j):
+ i = max(i, 0); j = max(j, 0)
+ return self.__class__(self.data()[i:j])
+ def __setslice__(self, i, j, other):
+ i = max(i, 0); j = max(j, 0)
+ if isinstance(other, UserList):
+ self.data()[i:j] = other.data()
+ elif isinstance(other, type(self.data())):
+ self.data()[i:j] = other
+ else:
+ self.data()[i:j] = list(other)
+ def __delslice__(self, i, j):
+ i = max(i, 0); j = max(j, 0)
+ del self.data()[i:j]
+ def __add__(self, other):
+ if isinstance(other, UserList):
+ return self.__class__(self.data() + other.data())
+ elif isinstance(other, type(self.data())):
+ return self.__class__(self.data() + other)
+ else:
+ return self.__class__(self.data() + list(other))
+ def __radd__(self, other):
+ if isinstance(other, UserList):
+ return self.__class__(other.data() + self.data())
+ elif isinstance(other, type(self.data())):
+ return self.__class__(other + self.data())
+ else:
+ return self.__class__(list(other) + self.data())
+ def __iadd__(self, other):
+ raise NotImplementedError, "Not implemented"
+
+ def __mul__(self, n):
+ return self.__class__(self.data()*n)
+ __rmul__ = __mul__
+ def __imul__(self, n):
+ raise NotImplementedError, "Not implemented"
+ def append(self, item): self.data().append(item)
+ def insert(self, i, item): self.data().insert(i, item)
+ def pop(self, i=-1): return self.data().pop(i)
+ def remove(self, item): self.data().remove(item)
+ def count(self, item): return self.data().count(item)
+ def index(self, item): return self.data().index(item)
+ def reverse(self): self.data().reverse()
+ def sort(self, *args): apply(self.data().sort, args)
+ def extend(self, other):
+ if isinstance(other, UserList):
+ self.data().extend(other.data())
+ else:
+ self.data().extend(other)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: Install.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+from Products.GroupUserFolder import groupuserfolder_globals
+from Products.GroupUserFolder.GroupUserFolder import GroupUserFolder
+from StringIO import StringIO
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.DirectoryView import addDirectoryViews
+from Acquisition import aq_base
+from OFS.Folder import manage_addFolder
+
+
+SKIN_NAME = "gruf"
+_globals = globals()
+
+def install_plone(self, out):
+ pass
+
+def install_subskin(self, out, skin_name=SKIN_NAME, globals=groupuserfolder_globals):
+ print >>out, " Installing subskin."
+ skinstool=getToolByName(self, 'portal_skins')
+ if skin_name not in skinstool.objectIds():
+ print >>out, " Adding directory view for GRUF"
+ addDirectoryViews(skinstool, 'skins', globals)
+
+ for skinName in skinstool.getSkinSelections():
+ path = skinstool.getSkinPath(skinName)
+ path = [i.strip() for i in path.split(',')]
+ try:
+ if skin_name not in path:
+ path.insert(path.index('custom') +1, skin_name)
+ except ValueError:
+ if skin_name not in path:
+ path.append(skin_name)
+
+ path = ','.join(path)
+ skinstool.addSkinSelection( skinName, path)
+ print >>out, " Done installing subskin."
+
+def walk(out, obj, operation):
+ if obj.isPrincipiaFolderish:
+ for content in obj.objectValues():
+ walk(out, content, operation)
+ operation(out, obj)
+
+
+def migrate_user_folder(obj, out, ):
+ """
+ Move a user folder into a temporary folder, create a GroupUserFolder,
+ and then move the old user folder into the Users portion of the GRUF.
+ NOTE: You cant copy/paste between CMF and Zope folder. *sigh*
+ """
+ id = obj.getId()
+ if id == 'acl_users':
+ if obj.__class__.__name__ == "GroupUserFolder":
+ # Avoid already-created GRUFs
+ print >>out, " Do NOT migrate acl_users at %s, as it is already a GroupUserFolder" % ('/'.join( obj.getPhysicalPath() ), )
+ return out.getvalue()
+
+ print >>out, " Migrating acl_users folder at %s to a GroupUserFolder" % ('/'.join( obj.getPhysicalPath() ), )
+
+ container = obj.aq_parent
+
+ # Instead of using Copy/Paste we hold a reference to the acl_users
+ # and use that reference instead of physically moving objects in ZODB
+ tmp_users=container._getOb('acl_users')
+ tmp_allow=container.__allow_groups__
+
+ del container.__allow_groups__
+ if 'acl_users' in container.objectIds():
+ container.manage_delObjects('acl_users')
+
+ container.manage_addProduct['GroupUserFolder'].manage_addGroupUserFolder()
+ container.acl_users.Users.manage_delObjects( 'acl_users' )
+ container.acl_users.Users._setObject('acl_users', aq_base(tmp_users))
+ container.__allow_groups__ = aq_base(getattr(container,'acl_users'))
+
+ return out.getvalue()
+
+
+def migrate_plone_site_to_gruf(self, out = None):
+ if out is None:
+ out = StringIO()
+ print >>out, " Attempting to migrate UserFolders to GroupUserFolders..."
+ urltool=getToolByName(self, 'portal_url')
+ plonesite = urltool.getPortalObject()
+ ## We disable the 'walk' operation because if the acl_users object is deep inside
+ ## the Plone site, that is a real problem. Furthermore, that may be because
+ ## we're already digging an GRUF and have the risk to update a GRUF/User/acl_users
+ ## object !
+## walk(out, plonesite, migrate_user_folder)
+ for obj in plonesite.objectValues():
+ migrate_user_folder(obj, out, )
+ print >>out, " Done Migrating UserFolders to GroupUserFolders."
+ return out.getvalue()
+
+def install(self):
+ out = StringIO()
+ print >>out, "Installing GroupUserFolder"
+ install_subskin(self, out)
+ install_plone(self, out)
+ migrate_plone_site_to_gruf(self, out)
+ print >>out, "Done."
+ return out.getvalue()
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: __init__.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: GRUFFolder.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+
+# fakes a method from a DTML file
+from Globals import MessageDialog, DTMLFile
+
+from AccessControl import ClassSecurityInfo
+from Globals import InitializeClass
+from Acquisition import Implicit
+from Globals import Persistent
+from AccessControl.Role import RoleManager
+from OFS.SimpleItem import Item
+from OFS.PropertyManager import PropertyManager
+from OFS import ObjectManager, SimpleItem
+from DateTime import DateTime
+from App import ImageFile
+
+#XXX PJ DynaList is very hairy - why vs. PerstList?
+# (see C__ac_roles__ class below for an explanation)
+import DynaList
+import AccessControl.Role, webdav.Collection
+import Products
+import os
+import string
+import shutil
+import random
+
+
+
+def manage_addGRUFUsers(self, id="Users", dtself=None,REQUEST=None,**ignored):
+ """ """
+ f=GRUFUsers(id)
+ self=self.this()
+ try: self._setObject(id, f)
+ except: return MessageDialog(
+ title ='Item Exists',
+ message='This object already contains a GRUFUsers Folder',
+ action ='%s/manage_main' % REQUEST['URL1'])
+ if REQUEST is not None:
+ REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
+
+def manage_addGRUFGroups(self, id="Groups", dtself=None,REQUEST=None,**ignored):
+ """ """
+ f=GRUFGroups(id)
+ self=self.this()
+ try: self._setObject(id, f)
+ except: return MessageDialog(
+ title ='Item Exists',
+ message='This object already contains a GRUFGroups Folder',
+ action ='%s/manage_main' % REQUEST['URL1'])
+ if REQUEST is not None:
+ REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
+
+class GRUFFolder(ObjectManager.ObjectManager, SimpleItem.Item):
+ isAnObjectManager=1
+ isPrincipiaFolderish=1
+ manage_main=DTMLFile('dtml/GRUFFolder_main', globals())
+ manage_options=( {'label':'Contents', 'action':'manage_main'}, ) + \
+ SimpleItem.Item.manage_options
+
+ security = ClassSecurityInfo()
+ def __creatable_by_emergency_user__(self): return 1
+
+ def __init__(self, id = None):
+ if id:
+ self.id = id
+ else:
+ self.id = self.default_id
+
+ def getId(self,):
+ if self.id:
+ return self.id
+ else:
+ return self.default_id # Used for b/w compatibility
+
+ def getUserSourceId(self,):
+ return self.getId()
+
+ def isValid(self,):
+ """
+ isValid(self,) => Return true if an acl_users is inside
+ """
+ if "acl_users" in self.objectIds():
+ return 1
+ return None
+
+ security.declarePublic('header_text')
+ def header_text(self,):
+ """
+ header_text(self,) => Text that appears in the content's
+ view heading zone
+ """
+ return ""
+
+ def getUserFolder(self,):
+ """
+ getUserFolder(self,) => get the underlying user folder, UNRESTRICTED !
+ """
+ if not "acl_users" in self.objectIds():
+ raise "ValueError", "Please put an acl_users in %s " \
+ "before using GRUF" % (self.getId(),)
+ return self.restrictedTraverse('acl_users')
+
+ def getUserNames(self,):
+ """
+ getUserNames(self,) => None
+
+ We override this to prevent SimpleUserFolder to use GRUF's getUserNames() method.
+ It's, of course, still possible to override a getUserNames method with SimpleUserFolder:
+ just call it 'new_getUserNames'.
+ """
+ # Call the "new_getUserNames" method if available
+ if "new_getUserNames" in self.objectIds():
+ return self.unrestrictedTraverse('new_getUserNames')()
+
+ # Return () if nothing is there
+ return ()
+
+
+
+
+class GRUFUsers(GRUFFolder):
+ """
+ GRUFUsers : GRUFFolder that holds users
+ """
+ meta_type="GRUFUsers"
+ default_id = "Users"
+
+ manage_options = GRUFFolder.manage_options
+
+ class C__ac_roles__(Persistent, Implicit, DynaList.DynaList):
+ """
+ __ac_roles__ dynastring.
+ Do not forget to set _target to class instance.
+
+ XXX DynaList is surely not efficient but it's the only way
+ I found to do what I wanted easily. Someone should take
+ a look to PerstList instead to see if it's possible
+ to do the same ? (ie. having a list which elements are
+ the results of a method call).
+
+ However, even if DynaList is not performant, it's not
+ a critical point because this list is meant to be
+ looked at only when a User object is looked at INSIDE
+ GRUF (especially to set groups a user belongs to).
+ So in practice only used within ZMI.
+ """
+ def data(self,):
+ return self.userdefined_roles()
+
+
+ # Property setting
+ ac_roles = C__ac_roles__()
+ __ac_roles__ = ac_roles
+
+ enabled = 1 # True if it's enabled, false if not
+
+ def enableSource(self,):
+ """enableSource(self,) => Set enable status to 1
+ """
+ self.enabled = 1
+
+ def disableSource(self,):
+ """disableSource(self,) => explicit ;)
+ """
+ self.enabled = None
+
+ def isEnabled(self,):
+ """
+ Return true if enabled (surprisingly)
+ """
+ return not not self.enabled
+
+ def header_text(self,):
+ """
+ header_text(self,) => Text that appears in the content's view
+ heading zone
+ """
+ if not "acl_users" in self.objectIds():
+ return "Please put an acl_users here before ever " \
+ "starting to use this object."
+
+ ret = """In this folder, groups are seen as ROLES from user's
+ view. To put a user into a group, affect him a role
+ that matches his group.<br />"""
+
+ return ret
+
+
+ def listGroups(self,):
+ """
+ listGroups(self,) => return a list of groups defined as roles
+ """
+ return self.Groups.restrictedTraverse('listGroups')()
+
+
+ def userdefined_roles(self):
+ "Return list of user-defined roles"
+ return self.listGroups()
+
+
+class GRUFGroups(GRUFFolder):
+ """
+ GRUFGroups : GRUFFolder that holds groups
+ """
+ meta_type="GRUFGroups"
+ default_id = "Groups"
+
+ _group_prefix = "group_"
+
+
+ class C__ac_roles__(Persistent, Implicit, DynaList.DynaList):
+ """
+ __ac_roles__ dynastring.
+ Do not forget to set _target to class instance.
+
+ XXX DynaList is surely not efficient but it's the only way
+ I found to do what I wanted easily. Someone should take
+ a look to PerstList instead to see if it's possible
+ to do the same ? (ie. having a list which elements are
+ the results of a method call).
+
+ However, even if DynaList is not performant, it's not
+ a critical point because this list is meant to be
+ looked at only when a User object is looked at INSIDE
+ GRUF (especially to set groups a user belongs to).
+ So in practice only used within ZMI.
+ """
+ def data(self,):
+ return self.userdefined_roles()
+
+
+ ac_roles = C__ac_roles__()
+ __ac_roles__ = ac_roles
+
+
+ def header_text(self,):
+ """
+ header_text(self,) => Text that appears in the content's
+ view heading zone
+ """
+ ret = ""
+ if not "acl_users" in self.objectIds():
+ return "Please put an acl_users here before ever " \
+ "starting to use this object."
+ return ret
+
+ def _getGroup(self, id):
+ """
+ _getGroup(self, id) => same as getUser() but... with a group :-)
+ This method will return an UNWRAPPED object
+ """
+ return self.acl_users.getUser(id)
+
+
+ def listGroups(self, prefixed = 1):
+ """
+ Return a list of available groups.
+ Group names are prefixed !
+ """
+ if not prefixed:
+ return self.acl_users.getUserNames()
+ else:
+ ret = []
+ for grp in self.acl_users.getUserNames():
+ ret.append("%s%s" % (self._group_prefix, grp))
+ return ret
+
+
+ def userdefined_roles(self):
+ "Return list of user-defined roles"
+ return self.listGroups()
+
+
+InitializeClass(GRUFUsers)
+InitializeClass(GRUFGroups)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: GRUFUser.py 40118 2007-04-01 15:13:44Z alecm $
+__docformat__ = 'restructuredtext'
+
+from copy import copy
+
+# fakes a method from a DTML File
+from Globals import MessageDialog, DTMLFile
+
+from AccessControl import ClassSecurityInfo
+from AccessControl import Permissions
+from AccessControl import getSecurityManager
+from Globals import InitializeClass
+from Acquisition import Implicit, aq_inner, aq_parent, aq_base
+from Globals import Persistent
+from AccessControl.Role import RoleManager
+from OFS.SimpleItem import Item
+from OFS.PropertyManager import PropertyManager
+from OFS import ObjectManager, SimpleItem
+from DateTime import DateTime
+from App import ImageFile
+import AccessControl.Role, webdav.Collection
+import Products
+import os
+import string
+import shutil
+import random
+from global_symbols import *
+import AccessControl
+from Products.GroupUserFolder import postonly
+import GRUFFolder
+import GroupUserFolder
+from AccessControl.PermissionRole \
+ import _what_not_even_god_should_do, rolesForPermissionOn
+from ComputedAttribute import ComputedAttribute
+
+
+import os
+import traceback
+
+from interfaces.IUserFolder import IUser, IGroup
+
+_marker = ['INVALID_VALUE']
+
+# NOTE : _what_not_even_god_should_do is a specific permission defined by ZOPE
+# that indicates that something has not to be done within Zope.
+# This value is given to the ACCESS_NONE directive of a SecurityPolicy.
+# It's rarely used within Zope BUT as it is documented (in AccessControl)
+# and may be used by third-party products, we have to process it.
+
+
+#GROUP_PREFIX is a constant
+
+class GRUFUserAtom(AccessControl.User.BasicUser, Implicit):
+ """
+ Base class for all GRUF-catched User objects.
+ There's, alas, many copy/paste from AccessControl.BasicUser...
+ """
+ security = ClassSecurityInfo()
+
+ security.declarePrivate('_setUnderlying')
+ def _setUnderlying(self, user):
+ """
+ _setUnderlying(self, user) => Set the GRUFUser properties to
+ the underlying user's one.
+ Be careful that any change to the underlying user won't be
+ reported here. $$$ We don't know yet if User object are
+ transaction-persistant or not...
+ """
+ self._original_name = user.getUserName()
+ self._original_password = user._getPassword()
+ self._original_roles = user.getRoles()
+ self._original_domains = user.getDomains()
+ self._original_id = user.getId()
+ self.__underlying__ = user # Used for authenticate() and __getattr__
+
+
+ # ----------------------------
+ # Public User object interface
+ # ----------------------------
+
+ # Maybe allow access to unprotected attributes. Note that this is
+ # temporary to avoid exposing information but without breaking
+ # everyone's current code. In the future the security will be
+ # clamped down and permission-protected here. Because there are a
+ # fair number of user object types out there, this method denies
+ # access to names that are private parts of the standard User
+ # interface or implementation only. The other approach (only
+ # allowing access to public names in the User interface) would
+ # probably break a lot of other User implementations with extended
+ # functionality that we cant anticipate from the base scaffolding.
+
+ security.declarePrivate('__init__')
+ def __init__(self, underlying_user, GRUF, isGroup, source_id, ):
+ # When calling, set isGroup it to TRUE if this user represents a group
+ self._setUnderlying(underlying_user)
+ self._isGroup = isGroup
+ self._GRUF = GRUF
+ self._source_id = source_id
+ self.id = self._original_id
+ # Store the results of getRoles and getGroups. Initially set to None,
+ # set to a list after the methods are first called.
+ # If you are caching users you want to clear these.
+ self.clearCachedGroupsAndRoles()
+
+ security.declarePrivate('clearCachedGroupsAndRoles')
+ def clearCachedGroupsAndRoles(self, underlying_user = None):
+ self._groups = None
+ self._user_roles = None
+ self._group_roles = None
+ self._all_roles = None
+ if underlying_user:
+ self._setUnderlying(underlying_user)
+ self._original_user_roles = None
+
+ security.declarePublic('isGroup')
+ def isGroup(self,):
+ """Return 1 if this user is a group abstraction"""
+ return self._isGroup
+
+ security.declarePublic('getUserSourceId')
+ def getUserSourceId(self,):
+ """
+ getUserSourceId(self,) => string
+ Return the GRUF's GRUFUsers folder used to fetch this user.
+ """
+ return self._source_id
+
+ security.declarePrivate('getGroupNames')
+ def getGroupNames(self,):
+ """..."""
+ ret = self._getGroups(no_recurse = 1)
+ return map(lambda x: x[GROUP_PREFIX_LEN:], ret)
+
+ security.declarePrivate('getGroupIds')
+ def getGroupIds(self,):
+ """..."""
+ return list(self._getGroups(no_recurse = 1))
+
+ security.declarePrivate("getAllGroups")
+ def getAllGroups(self,):
+ """Same as getAllGroupNames()"""
+ return self.getAllGroupIds()
+
+ security.declarePrivate('getAllGroupNames')
+ def getAllGroupNames(self,):
+ """..."""
+ ret = self._getGroups()
+ return map(lambda x: x[GROUP_PREFIX_LEN:], ret)
+
+ security.declarePrivate('getAllGroupIds')
+ def getAllGroupIds(self,):
+ """..."""
+ return list(self._getGroups())
+
+ security.declarePrivate('getGroups')
+ def getGroups(self, *args, **kw):
+ """..."""
+ ret = self._getGroups(*args, **kw)
+ return list(ret)
+
+ security.declarePrivate("getImmediateGroups")
+ def getImmediateGroups(self,):
+ """
+ Return NON-TRANSITIVE groups
+ """
+ ret = self._getGroups(no_recurse = 1)
+ return list(ret)
+
+ def _getGroups(self, no_recurse = 0, already_done = None, prefix = GROUP_PREFIX):
+ """
+ getGroups(self, no_recurse = 0, already_done = None, prefix = GROUP_PREFIX) => list of strings
+
+ If this user is a user (uh, uh), get its groups.
+ THIS METHODS NOW SUPPORTS NESTED GROUPS ! :-)
+ The already_done parameter prevents infite recursions.
+ Keep it as it is, never give it a value.
+
+ If no_recurse is true, return only first level groups
+
+ This method is private and should remain so.
+ """
+ if already_done is None:
+ already_done = []
+
+ # List this user's roles. We consider that roles starting
+ # with GROUP_PREFIX are in fact groups, and thus are
+ # returned (prefixed).
+ if self._groups is not None:
+ return self._groups
+
+ # Populate cache if necessary
+ if self._original_user_roles is None:
+ self._original_user_roles = self.__underlying__.getRoles()
+
+ # Scan roles to find groups
+ ret = []
+ for role in self._original_user_roles:
+ # Inspect group-like roles
+ if role.startswith(prefix):
+
+ # Prevent infinite recursion
+ if self._isGroup and role in already_done:
+ continue
+
+ # Get the underlying group
+ grp = self.aq_parent.getUser(role)
+ if not grp:
+ continue # Invalid group
+
+ # Do not add twice the current group
+ if role in ret:
+ continue
+
+ # Append its nested groups (if recurse is asked)
+ ret.append(role)
+ if no_recurse:
+ continue
+ for extend in grp.getGroups(already_done = ret):
+ if not extend in ret:
+ ret.append(extend)
+
+ # Return the groups
+ self._groups = tuple(ret)
+ return self._groups
+
+
+ security.declarePrivate('getGroupsWithoutPrefix')
+ def getGroupsWithoutPrefix(self, **kw):
+ """
+ Same as getGroups but return them without a prefix.
+ """
+ ret = []
+ for group in self.getGroups(**kw):
+ if group.startswith(GROUP_PREFIX):
+ ret.append(group[len(GROUP_PREFIX):])
+ return ret
+
+ security.declarePublic('getUserNameWithoutGroupPrefix')
+ def getUserNameWithoutGroupPrefix(self):
+ """Return the username of a user without a group prefix"""
+ if self.isGroup() and \
+ self._original_name[:len(GROUP_PREFIX)] == GROUP_PREFIX:
+ return self._original_name[len(GROUP_PREFIX):]
+ return self._original_name
+
+ security.declarePublic('getUserId')
+ def getUserId(self):
+ """Return the user id of a user"""
+ if self.isGroup() and \
+ not self._original_name[:len(GROUP_PREFIX)] == GROUP_PREFIX:
+ return "%s%s" % (GROUP_PREFIX, self._original_name )
+ return self._original_name
+
+ security.declarePublic("getName")
+ def getName(self,):
+ """Get user's or group's name.
+ For a user, the name can be set by the underlying user folder but usually id == name.
+ For a group, the ID is prefixed, but the NAME is NOT prefixed by 'group_'.
+ """
+ return self.getUserNameWithoutGroupPrefix()
+
+ security.declarePublic("getUserName")
+ def getUserName(self,):
+ """Alias for getName()"""
+ return self.getUserNameWithoutGroupPrefix()
+
+ security.declarePublic('getId')
+ def getId(self, unprefixed = 0):
+ """Get the ID of the user. The ID can be used, at least from
+ Python, to get the user from the user's UserDatabase
+ """
+ # Return the right id
+ if self.isGroup() and not self._original_name.startswith(GROUP_PREFIX) and not unprefixed:
+ return "%s%s" % (GROUP_PREFIX, self._original_name)
+ return self._original_name
+
+ security.declarePublic('getRoles')
+ def getRoles(self):
+ """
+ Return the list (tuple) of roles assigned to a user.
+ THIS IS WHERE THE ATHENIANS REACHED !
+ """
+ if self._all_roles is not None:
+ return self._all_roles
+
+ # Return user and groups roles
+ self._all_roles = GroupUserFolder.unique(self.getUserRoles() + self.getGroupRoles())
+ return self._all_roles
+
+ security.declarePublic('getUserRoles')
+ def getUserRoles(self):
+ """
+ returns the roles defined for the user without the group roles
+ """
+ if self._user_roles is not None:
+ return self._user_roles
+ prefix = GROUP_PREFIX
+ if self._original_user_roles is None:
+ self._original_user_roles = self.__underlying__.getRoles()
+ self._user_roles = tuple([r for r in self._original_user_roles if not r.startswith(prefix)])
+ return self._user_roles
+
+ security.declarePublic("getGroupRoles")
+ def getGroupRoles(self,):
+ """
+ Return the tuple of roles belonging to this user's group(s)
+ """
+ if self._group_roles is not None:
+ return self._group_roles
+ ret = []
+ acl_users = self._GRUF.acl_users
+ groups = acl_users.getGroupIds() # XXX We can have a cache here
+
+ for group in self.getGroups():
+ if not group in groups:
+ Log("Group", group, "is invalid. Ignoring.")
+ # This may occur when groups are deleted
+ # Ignored silently
+ continue
+ ret.extend(acl_users.getGroup(group).getUserRoles())
+
+ self._group_roles = GroupUserFolder.unique(ret)
+ return self._group_roles
+
+ security.declarePublic('getRolesInContext')
+ def getRolesInContext(self, object, userid = None):
+ """
+ Return the list of roles assigned to the user,
+ including local roles assigned in context of
+ the passed in object.
+ """
+ if not userid:
+ userid=self.getId()
+
+ roles = {}
+ for role in self.getRoles():
+ roles[role] = 1
+
+ user_groups = self.getGroups()
+
+ inner_obj = getattr(object, 'aq_inner', object)
+ while 1:
+ # Usual local roles retreiving
+ local_roles = getattr(inner_obj, '__ac_local_roles__', None)
+ if local_roles:
+ if callable(local_roles):
+ local_roles = local_roles()
+ dict = local_roles or {}
+
+ for role in dict.get(userid, []):
+ roles[role] = 1
+
+ # Get roles & local roles for groups
+ # This handles nested groups as well
+ for groupid in user_groups:
+ for role in dict.get(groupid, []):
+ roles[role] = 1
+
+ # LocalRole blocking
+ obj = getattr(inner_obj, 'aq_base', inner_obj)
+ if getattr(obj, '__ac_local_roles_block__', None):
+ break
+
+ # Loop management
+ inner = getattr(inner_obj, 'aq_inner', inner_obj)
+ parent = getattr(inner, 'aq_parent', None)
+ if parent is not None:
+ inner_obj = parent
+ continue
+ if hasattr(inner_obj, 'im_self'):
+ inner_obj=inner_obj.im_self
+ inner_obj=getattr(inner_obj, 'aq_inner', inner_obj)
+ continue
+ break
+
+ return tuple(roles.keys())
+
+ security.declarePublic('getDomains')
+ def getDomains(self):
+ """Return the list of domain restrictions for a user"""
+ return self._original_domains
+
+
+ security.declarePrivate("getProperty")
+ def getProperty(self, name, default=_marker):
+ """getProperty(self, name) => return property value or raise AttributeError
+ """
+ # Try to do an attribute lookup on the underlying user object
+ v = getattr(self.__underlying__, name, default)
+ if v is _marker:
+ raise AttributeError, name
+ return v
+
+ security.declarePrivate("hasProperty")
+ def hasProperty(self, name):
+ """hasProperty"""
+ return hasattr(self.__underlying__, name)
+
+ security.declarePrivate("setProperty")
+ def setProperty(self, name, value):
+ """setProperty => Try to set the property...
+ By now, it's available only for LDAPUserFolder
+ """
+ # Get actual source
+ src = self._GRUF.getUserSource(self.getUserSourceId())
+ if not src:
+ raise RuntimeError, "Invalid or missing user source for '%s'." % (self.getId(),)
+
+ # LDAPUserFolder => specific API.
+ if hasattr(src, "manage_setUserProperty"):
+ # Unmap pty name if necessary, get it in the schema
+ ldapname = None
+ for schema in src.getSchemaConfig().values():
+ if schema["ldap_name"] == name:
+ ldapname = schema["ldap_name"]
+ if schema["public_name"] == name:
+ ldapname = schema["ldap_name"]
+ break
+
+ # If we didn't find it, we skip it
+ if ldapname is None:
+ raise KeyError, "Invalid LDAP attribute: '%s'." % (name, )
+
+ # Edit user
+ user_dn = src._find_user_dn(self.getUserName())
+ src.manage_setUserProperty(user_dn, ldapname, value)
+
+ # Expire the underlying user object
+ self.__underlying__ = src.getUser(self.getId())
+ if not self.__underlying__:
+ raise RuntimeError, "Error while setting property of '%s'." % (self.getId(),)
+
+ # Now we check if the property has been changed
+ if not self.hasProperty(name):
+ raise NotImplementedError, "Property setting is not supported for '%s'." % (name,)
+ v = self._GRUF.getUserById(self.getId()).getProperty(name)
+ if not v == value:
+ Log(LOG_DEBUG, "Property '%s' for user '%s' should be '%s' and not '%s'" % (
+ name, self.getId(), value, v,
+ ))
+ raise NotImplementedError, "Property setting is not supported for '%s'." % (name,)
+
+ # ------------------------------
+ # Internal User object interface
+ # ------------------------------
+
+ security.declarePrivate('authenticate')
+ def authenticate(self, password, request):
+ # We prevent groups from authenticating
+ if self._isGroup:
+ return None
+ return self.__underlying__.authenticate(password, request)
+
+
+ security.declarePublic('allowed')
+ def allowed(self, object, object_roles=None):
+ """Check whether the user has access to object. The user must
+ have one of the roles in object_roles to allow access."""
+
+ if object_roles is _what_not_even_god_should_do:
+ return 0
+
+ # Short-circuit the common case of anonymous access.
+ if object_roles is None or 'Anonymous' in object_roles:
+ return 1
+
+ # Provide short-cut access if object is protected by 'Authenticated'
+ # role and user is not nobody
+ if 'Authenticated' in object_roles and \
+ (self.getUserName() != 'Anonymous User'):
+ return 1
+
+ # Check for ancient role data up front, convert if found.
+ # This should almost never happen, and should probably be
+ # deprecated at some point.
+ if 'Shared' in object_roles:
+ object_roles = self._shared_roles(object)
+ if object_roles is None or 'Anonymous' in object_roles:
+ return 1
+
+
+ # Trying to make some speed improvements, changes starts here.
+ # Helge Tesdal, Plone Solutions AS, http://www.plonesolutions.com
+ # We avoid using the getRoles() and getRolesInContext() methods to be able
+ # to short circuit.
+
+ # Dict for faster lookup and avoiding duplicates
+ object_roles_dict = {}
+ for role in object_roles:
+ object_roles_dict[role] = 1
+
+ if [role for role in self.getUserRoles() if object_roles_dict.has_key(role)]:
+ if self._check_context(object):
+ return 1
+ return None
+
+ # Try the top level group roles.
+ if [role for role in self.getGroupRoles() if object_roles_dict.has_key(role)]:
+ if self._check_context(object):
+ return 1
+ return None
+
+ user_groups = self.getGroups()
+ # No luck on the top level, try local roles
+ inner_obj = getattr(object, 'aq_inner', object)
+ userid = self.getId()
+ while 1:
+ local_roles = getattr(inner_obj, '__ac_local_roles__', None)
+ if local_roles:
+ if callable(local_roles):
+ local_roles = local_roles()
+ dict = local_roles or {}
+
+ if [role for role in dict.get(userid, []) if object_roles_dict.has_key(role)]:
+ if self._check_context(object):
+ return 1
+ return None
+
+ # Get roles & local roles for groups
+ # This handles nested groups as well
+ for groupid in user_groups:
+ if [role for role in dict.get(groupid, []) if object_roles_dict.has_key(role)]:
+ if self._check_context(object):
+ return 1
+ return None
+
+ # LocalRole blocking
+ obj = getattr(inner_obj, 'aq_base', inner_obj)
+ if getattr(obj, '__ac_local_roles_block__', None):
+ break
+
+ # Loop control
+ inner = getattr(inner_obj, 'aq_inner', inner_obj)
+ parent = getattr(inner, 'aq_parent', None)
+ if parent is not None:
+ inner_obj = parent
+ continue
+ if hasattr(inner_obj, 'im_self'):
+ inner_obj=inner_obj.im_self
+ inner_obj=getattr(inner_obj, 'aq_inner', inner_obj)
+ continue
+ break
+ return None
+
+
+ security.declarePublic('hasRole')
+ def hasRole(self, *args, **kw):
+ """hasRole is an alias for 'allowed' and has been deprecated.
+
+ Code still using this method should convert to either 'has_role' or
+ 'allowed', depending on the intended behaviour.
+
+ """
+ import warnings
+ warnings.warn('BasicUser.hasRole is deprecated, please use '
+ 'BasicUser.allowed instead; hasRole was an alias for allowed, but '
+ 'you may have ment to use has_role.', DeprecationWarning)
+ return self.allowed(*args, **kw)
+
+ # #
+ # Underlying user object support #
+ # #
+
+ def __getattr__(self, name):
+ # This will call the underlying object's methods
+ # if they are not found in this user object.
+ # We will have to check Chris' http://www.plope.com/Members/chrism/plone_on_zope_head
+ # to make it work with Zope HEAD.
+ ret = getattr(self.__dict__['__underlying__'], name)
+ return ret
+
+ security.declarePublic('getUnwrappedUser')
+ def getUnwrappedUser(self,):
+ """
+ same as GRUF.getUnwrappedUser, but implicitly with this particular user
+ """
+ return self.__dict__['__underlying__']
+
+ def __getitem__(self, name):
+ # This will call the underlying object's methods
+ # if they are not found in this user object.
+ return self.__underlying__[name]
+
+ # #
+ # HTML link support #
+ # #
+
+ def asHTML(self, implicit=0):
+ """
+ asHTML(self, implicit=0) => HTML string
+ Used to generate homogeneous links for management screens
+ """
+ acl_users = self.acl_users
+ if self.isGroup():
+ color = acl_users.group_color
+ kind = "Group"
+ else:
+ color = acl_users.user_color
+ kind = "User"
+
+ ret = '''<a href="%(href)s" alt="%(alt)s"><font color="%(color)s">%(name)s</font></a>''' % {
+ "color": color,
+ "href": "%s/%s/manage_workspace?FORCE_USER=1" % (acl_users.absolute_url(), self.getId(), ),
+ "name": self.getUserNameWithoutGroupPrefix(),
+ "alt": "%s (%s)" % (self.getUserNameWithoutGroupPrefix(), kind, ),
+ }
+ if implicit:
+ return "<i>%s</i>" % ret
+ return ret
+
+
+ security.declarePrivate("isInGroup")
+ def isInGroup(self, groupid):
+ """Return true if the user is member of the specified group id
+ (including transitive groups)"""
+ return groupid in self.getAllGroupIds()
+
+ security.declarePublic("getRealId")
+ def getRealId(self,):
+ """Return id WITHOUT group prefix
+ """
+ raise NotImplementedError, "Must be derived in subclasses"
+
+
+class GRUFUser(GRUFUserAtom):
+ """
+ This is the class for actual user objects
+ """
+ __implements__ = (IUser, )
+
+ security = ClassSecurityInfo()
+
+ # #
+ # User Mutation #
+ # #
+
+ security.declarePublic('changePassword')
+ def changePassword(self, password, REQUEST=None):
+ """Set the user's password. This method performs its own security checks"""
+ # Check security
+ user = getSecurityManager().getUser()
+ if not user.has_permission(Permissions.manage_users, self._GRUF): # Is manager ?
+ if user.__class__.__name__ != "GRUFUser":
+ raise "Unauthorized", "You cannot change someone else's password."
+ if not user.getId() == self.getId(): # Is myself ?
+ raise "Unauthorized", "You cannot change someone else's password."
+
+ # Just do it
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userSetPassword(self.getId(), password)
+ changePassword = postonly(changePassword)
+
+ security.declarePrivate("setRoles")
+ def setRoles(self, roles):
+ """Change the roles of a user atom.
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userSetRoles(self.getId(), roles)
+
+ security.declarePrivate("addRole")
+ def addRole(self, role):
+ """Append a role for a user atom
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userAddRole(self.getId(), role)
+
+ security.declarePrivate("removeRole")
+ def removeRole(self, role):
+ """Remove the role of a user atom
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userRemoveRole(self.getId(), role)
+
+ security.declarePrivate("setPassword")
+ def setPassword(self, newPassword):
+ """Set the password of a user
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userSetPassword(self.getId(), newPassword)
+
+ security.declarePrivate("setDomains")
+ def setDomains(self, domains):
+ """Set domains for a user
+ """
+ self.clearCachedGroupsAndRoles()
+ self._GRUF.userSetDomains(self.getId(), domains)
+ self._original_domains = self._GRUF.userGetDomains(self.getId())
+
+ security.declarePrivate("addDomain")
+ def addDomain(self, domain):
+ """Append a domain to a user
+ """
+ self.clearCachedGroupsAndRoles()
+ self._GRUF.userAddDomain(self.getId(), domain)
+ self._original_domains = self._GRUF.userGetDomains(self.getId())
+
+ security.declarePrivate("removeDomain")
+ def removeDomain(self, domain):
+ """Remove a domain from a user
+ """
+ self.clearCachedGroupsAndRoles()
+ self._GRUF.userRemoveDomain(self.getId(), domain)
+ self._original_domains = self._GRUF.userGetDomains(self.getId())
+
+ security.declarePrivate("setGroups")
+ def setGroups(self, groupnames):
+ """Set the groups of a user
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userSetGroups(self.getId(), groupnames)
+
+ security.declarePrivate("addGroup")
+ def addGroup(self, groupname):
+ """add a group to a user atom
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userAddGroup(self.getId(), groupname)
+
+ security.declarePrivate("removeGroup")
+ def removeGroup(self, groupname):
+ """remove a group from a user atom.
+ """
+ self.clearCachedGroupsAndRoles()
+ return self._GRUF.userRemoveGroup(self.getId(), groupname)
+
+ security.declarePrivate('_getPassword')
+ def _getPassword(self):
+ """Return the password of the user."""
+ return self._original_password
+
+ security.declarePublic("getRealId")
+ def getRealId(self,):
+ """Return id WITHOUT group prefix
+ """
+ return self.getId()
+
+
+class GRUFGroup(GRUFUserAtom):
+ """
+ This is the class for actual group objects
+ """
+ __implements__ = (IGroup, )
+
+ security = ClassSecurityInfo()
+
+ security.declarePublic("getRealId")
+ def getRealId(self,):
+ """Return group id WITHOUT group prefix
+ """
+ return self.getId()[len(GROUP_PREFIX):]
+
+ def _getLDAPMemberIds(self,):
+ """
+ _getLDAPMemberIds(self,) => Uses LDAPUserFolder to find
+ users in a group.
+ """
+ # Find the right source
+ gruf = self.aq_parent
+ src = None
+ for src in gruf.listUserSources():
+ if not src.meta_type == "LDAPUserFolder":
+ continue
+ if src is None:
+ Log(LOG_DEBUG, "No LDAPUserFolder source found")
+ return []
+
+ # Find the group in LDAP
+ groups = src.getGroups()
+ groupid = self.getId()
+ grp = [ group for group in groups if group[0] == self.getId() ]
+ if not grp:
+ Log(LOG_DEBUG, "No such group ('%s') found." % (groupid,))
+ return []
+
+ # Return the grup member ids
+ userids = src.getGroupedUsers(grp)
+ Log(LOG_DEBUG, "We've found %d users belonging to the group '%s'" % (len(userids), grp), )
+ return userids
+
+ def _getMemberIds(self, users = 1, groups = 1, transitive = 1, ):
+ """
+ Return the member ids (users and groups) of the atoms of this group.
+ Transitiveness attribute is ignored with LDAP (no nested groups with
+ LDAP anyway).
+ This method now uses a shortcut to fetch members of an LDAP group
+ (stored either within Zope or within your LDAP server)
+ """
+ # Initial parameters.
+ # We fetch the users/groups list depending on what we search,
+ # and carefuly avoiding to use LDAP sources.
+ gruf = self.aq_parent
+ ldap_sources = []
+ lst = []
+ if transitive:
+ method = "getAllGroupIds"
+ else:
+ method = "getGroupIds"
+ if users:
+ for src in gruf.listUserSources():
+ if src.meta_type == 'LDAPUserFolder':
+ ldap_sources.append(src)
+ continue # We'll fetch 'em later
+ lst.extend(src.getUserNames())
+ if groups:
+ lst.extend(gruf.getGroupIds())
+
+ # First extraction for regular user sources.
+ # This part is very very long, and the more users you have,
+ # the longer this method will be.
+ groupid = self.getId()
+ groups_mapping = {}
+ for u in lst:
+ usr = gruf.getUser(u)
+ if not usr:
+ groups_mapping[u] = []
+ Log(LOG_WARNING, "Invalid user retreiving:", u)
+ else:
+ groups_mapping[u] = getattr(usr, method)()
+ members = [u for u in lst if groupid in groups_mapping[u]]
+
+ # If we have LDAP sources, we fetch user-group mapping inside directly
+ groupid = self.getId()
+ for src in ldap_sources:
+ groups = src.getGroups()
+ # With LDAPUserFolder >= 2.7 we need to add GROUP_PREFIX to group_name
+ # We keep backward compatibility
+ grp = [ group for group in groups if group[0] == self.getId() or \
+ GROUP_PREFIX + group[0] == self.getId()]
+ if not grp:
+ Log(LOG_DEBUG, "No such group ('%s') found." % (groupid,))
+ continue
+
+ # Return the grup member ids
+ userids = [ str(u) for u in src.getGroupedUsers(grp) ]
+ Log(LOG_DEBUG, "We've found %d users belonging to the group '%s'" % (len(userids), grp), )
+ members.extend(userids)
+
+ # Return the members we've found
+ return members
+
+ security.declarePrivate("getMemberIds")
+ def getMemberIds(self, transitive = 1, ):
+ "Return member ids of this group, including or not transitive groups."
+ return self._getMemberIds(transitive = transitive)
+
+ security.declarePrivate("getUserMemberIds")
+ def getUserMemberIds(self, transitive = 1, ):
+ """Return the member ids (users only) of the users of this group"""
+ return self._getMemberIds(groups = 0, transitive = transitive)
+
+ security.declarePrivate("getGroupMemberIds")
+ def getGroupMemberIds(self, transitive = 1, ):
+ """Return the members ids (groups only) of the groups of this group"""
+ return self._getMemberIds(users = 0, transitive = transitive)
+
+ security.declarePrivate("hasMember")
+ def hasMember(self, id):
+ """Return true if the specified atom id is in the group.
+ This is the contrary of IUserAtom.isInGroup(groupid)"""
+ gruf = self.aq_parent
+ return id in gruf.getMemberIds(self.getId())
+
+ security.declarePrivate("addMember")
+ def addMember(self, userid):
+ """Add a user the the current group"""
+ gruf = self.aq_parent
+ groupid = self.getId()
+ usr = gruf.getUser(userid)
+ if not usr:
+ raise ValueError, "Invalid user: '%s'" % (userid, )
+ if not groupid in gruf.getGroupNames() + gruf.getGroupIds():
+ raise ValueError, "Invalid group: '%s'" % (groupid, )
+ groups = list(usr.getGroups())
+ groups.append(groupid)
+ groups = GroupUserFolder.unique(groups)
+ return gruf._updateUser(userid, groups = groups)
+
+ security.declarePrivate("removeMember")
+ def removeMember(self, userid):
+ """Remove a user from the current group"""
+ gruf = self.aq_parent
+ groupid = self.getId()
+
+ # Check the user
+ usr = gruf.getUser(userid)
+ if not usr:
+ raise ValueError, "Invalid user: '%s'" % (userid, )
+
+ # Now, remove the group
+ groups = list(usr.getImmediateGroups())
+ if groupid in groups:
+ groups.remove(groupid)
+ gruf._updateUser(userid, groups = groups)
+ else:
+ raise ValueError, "User '%s' doesn't belong to group '%s'" % (userid, groupid, )
+
+ security.declarePrivate("setMembers")
+ def setMembers(self, userids):
+ """Set the members of the group
+ """
+ member_ids = self.getMemberIds()
+ all_ids = copy(member_ids)
+ all_ids.extend(userids)
+ groupid = self.getId()
+ for id in all_ids:
+ if id in member_ids and id not in userids:
+ self.removeMember(id)
+ elif id not in member_ids and id in userids:
+ self.addMember(id)
+
+
+InitializeClass(GRUFUser)
+InitializeClass(GRUFGroup)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+## Copyright (c) 2003 The Connexions Project, All Rights Reserved
+## initially written by J Cameron Cooper, 11 June 2003
+## concept with Brent Hendricks, George Runyan
+"""
+Basic group data tool.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: GroupDataTool.py 52136 2007-10-21 20:38:00Z encolpe $
+__docformat__ = 'restructuredtext'
+
+from Products.CMFCore.utils import UniqueObject, getToolByName
+from OFS.SimpleItem import SimpleItem
+from OFS.PropertyManager import PropertyManager
+from Globals import DTMLFile
+from Globals import InitializeClass
+from AccessControl.Role import RoleManager
+from BTrees.OOBTree import OOBTree
+from ZPublisher.Converters import type_converters
+from Acquisition import aq_inner, aq_parent, aq_base
+from AccessControl import ClassSecurityInfo, Permissions, Unauthorized, getSecurityManager
+
+from Products.CMFCore.ActionProviderBase import ActionProviderBase
+# BBB CMF < 1.5
+try:
+ from Products.CMFCore.permissions import ManagePortal
+except ImportError:
+ from Products.CMFCore.CMFCorePermissions import ManagePortal
+
+from Products.CMFCore.MemberDataTool import CleanupTemp
+
+from interfaces.portal_groupdata import portal_groupdata as IGroupDataTool
+from interfaces.portal_groupdata import GroupData as IGroupData
+from Products.GroupUserFolder import postonly
+from Products.GroupUserFolder.GRUFUser import GRUFGroup
+
+_marker = [] # Create a new marker object.
+
+from global_symbols import *
+
+
+class GroupDataTool (UniqueObject, SimpleItem, PropertyManager, ActionProviderBase):
+ """ This tool wraps group objects, allowing transparent access to properties.
+ """
+ # The latter will work only with Plone 1.1 => hence, the if
+ __implements__ = (IGroupDataTool, ActionProviderBase.__implements__)
+
+ id = 'portal_groupdata'
+ meta_type = 'CMF Group Data Tool'
+ _actions = ()
+
+ _v_temps = None
+ _properties=({'id':'title', 'type': 'string', 'mode': 'wd'},)
+
+ security = ClassSecurityInfo()
+
+ manage_options=( ActionProviderBase.manage_options +
+ ({ 'label' : 'Overview'
+ , 'action' : 'manage_overview'
+ },
+ )
+ + PropertyManager.manage_options
+ + SimpleItem.manage_options
+ )
+
+ #
+ # ZMI methods
+ #
+ security.declareProtected(ManagePortal, 'manage_overview')
+ manage_overview = DTMLFile('dtml/explainGroupDataTool', globals())
+
+ def __init__(self):
+ self._members = OOBTree()
+ # Create the default properties.
+ self._setProperty('description', '', 'text')
+ self._setProperty('email', '', 'string')
+
+ #
+ # 'portal_groupdata' interface methods
+ #
+ security.declarePrivate('wrapGroup')
+ def wrapGroup(self, g):
+ """Returns an object implementing the GroupData interface"""
+ id = g.getId()
+ members = self._members
+ if not members.has_key(id):
+ # Get a temporary member that might be
+ # registered later via registerMemberData().
+ temps = self._v_temps
+ if temps is not None and temps.has_key(id):
+ portal_group = temps[id]
+ else:
+ base = aq_base(self)
+ portal_group = GroupData(base, id)
+ if temps is None:
+ self._v_temps = {id:portal_group}
+ if hasattr(self, 'REQUEST'):
+ # No REQUEST during tests.
+ self.REQUEST._hold(CleanupTemp(self))
+ else:
+ temps[id] = portal_group
+ else:
+ portal_group = members[id]
+ # Return a wrapper with self as containment and
+ # the user as context.
+ return portal_group.__of__(self).__of__(g)
+
+ security.declarePrivate('registerGroupData')
+ def registerGroupData(self, g, id):
+ '''
+ Adds the given member data to the _members dict.
+ This is done as late as possible to avoid side effect
+ transactions and to reduce the necessary number of
+ entries.
+ '''
+ self._members[id] = aq_base(g)
+
+InitializeClass(GroupDataTool)
+
+
+class GroupData (SimpleItem):
+
+ __implements__ = IGroupData
+
+ security = ClassSecurityInfo()
+
+ id = None
+ _tool = None
+
+ def __init__(self, tool, id):
+ self.id = id
+ # Make a temporary reference to the tool.
+ # The reference will be removed by notifyModified().
+ self._tool = tool
+
+ def _getGRUF(self,):
+ return self.acl_users
+
+ security.declarePrivate('notifyModified')
+ def notifyModified(self):
+ # Links self to parent for full persistence.
+ tool = getattr(self, '_tool', None)
+ if tool is not None:
+ del self._tool
+ tool.registerGroupData(self, self.getId())
+
+ security.declarePublic('getGroup')
+ def getGroup(self):
+ """ Returns the actual group implementation. Varies by group
+ implementation (GRUF/Nux/et al). In GRUF this is a user object."""
+ # The user object is our context, but it's possible for
+ # restricted code to strip context while retaining
+ # containment. Therefore we need a simple security check.
+ parent = aq_parent(self)
+ bcontext = aq_base(parent)
+ bcontainer = aq_base(aq_parent(aq_inner(self)))
+ if bcontext is bcontainer or not hasattr(bcontext, 'getUserName'):
+ raise 'GroupDataError', "Can't find group data"
+ # Return the user object, which is our context.
+ return parent
+
+ def getTool(self):
+ return aq_parent(aq_inner(self))
+
+ security.declarePublic("getGroupMemberIds")
+ def getGroupMemberIds(self,):
+ """
+ Return a list of group member ids
+ """
+ return map(lambda x: x.getMemberId(), self.getGroupMembers())
+
+ security.declarePublic("getAllGroupMemberIds")
+ def getAllGroupMemberIds(self,):
+ """
+ Return a list of group member ids
+ """
+ return map(lambda x: x.getMemberId(), self.getAllGroupMembers())
+
+ security.declarePublic('getGroupMembers')
+ def getGroupMembers(self, ):
+ """
+ Returns a list of the portal_memberdata-ish members of the group.
+ This doesn't include TRANSITIVE groups/users.
+ """
+ md = self.portal_memberdata
+ gd = self.portal_groupdata
+ ret = []
+ for u_name in self.getGroup().getMemberIds(transitive = 0, ):
+ usr = self._getGRUF().getUserById(u_name)
+ if not usr:
+ raise AssertionError, "Cannot retreive a user by its id !"
+ if usr.isGroup():
+ ret.append(gd.wrapGroup(usr))
+ else:
+ ret.append(md.wrapUser(usr))
+ return ret
+
+ security.declarePublic('getAllGroupMembers')
+ def getAllGroupMembers(self, ):
+ """
+ Returns a list of the portal_memberdata-ish members of the group.
+ This will include transitive groups / users
+ """
+ md = self.portal_memberdata
+ gd = self.portal_groupdata
+ ret = []
+ for u_name in self.getGroup().getMemberIds():
+ usr = self._getGRUF().getUserById(u_name)
+ if not usr:
+ raise AssertionError, "Cannot retreive a user by its id !"
+ if usr.isGroup():
+ ret.append(gd.wrapGroup(usr))
+ else:
+ ret.append(md.wrapUser(usr))
+ return ret
+
+ def _getGroup(self,):
+ """
+ _getGroup(self,) => Get the underlying group object
+ """
+ return self._getGRUF().getGroupByName(self.getGroupName())
+
+
+ security.declarePrivate("canAdministrateGroup")
+ def canAdministrateGroup(self,):
+ """
+ Return true if the #current# user can administrate this group
+ """
+ user = getSecurityManager().getUser()
+ tool = self.getTool()
+ portal = getToolByName(tool, 'portal_url').getPortalObject()
+
+ # Has manager users pemission?
+ if user.has_permission(Permissions.manage_users, portal):
+ return True
+
+ # Is explicitly mentioned as a group administrator?
+ managers = self.getProperty('delegated_group_member_managers', ())
+ if user.getId() in managers:
+ return True
+
+ # Belongs to a group which is explicitly mentionned as a group administrator
+ meth = getattr(user, "getAllGroupNames", None)
+ if meth:
+ groups = meth()
+ else:
+ groups = ()
+ for v in groups:
+ if v in managers:
+ return True
+
+ # No right to edit this: we complain.
+ return False
+
+ security.declarePublic('addMember')
+ def addMember(self, id, REQUEST=None):
+ """ Add the existing member with the given id to the group"""
+ # We check if the current user can directly or indirectly administrate this group
+ if not self.canAdministrateGroup():
+ raise Unauthorized, "You cannot add a member to the group."
+ self._getGroup().addMember(id)
+
+ # Notify member that they've been changed
+ mtool = getToolByName(self, 'portal_membership')
+ member = mtool.getMemberById(id)
+ if member:
+ member.notifyModified()
+ addMember = postonly(addMember)
+
+ security.declarePublic('removeMember')
+ def removeMember(self, id, REQUEST=None):
+ """Remove the member with the provided id from the group.
+ """
+ # We check if the current user can directly or indirectly administrate this group
+ if not self.canAdministrateGroup():
+ raise Unauthorized, "You cannot remove a member from the group."
+ self._getGroup().removeMember(id)
+
+ # Notify member that they've been changed
+ mtool = getToolByName(self, 'portal_membership')
+ member = mtool.getMemberById(id)
+ if member:
+ member.notifyModified()
+ removeMember = postonly(removeMember)
+
+ security.declareProtected(Permissions.manage_users, 'setProperties')
+ def setProperties(self, properties=None, **kw):
+ '''Allows the manager group to set his/her own properties.
+ Accepts either keyword arguments or a mapping for the "properties"
+ argument.
+ '''
+ if properties is None:
+ properties = kw
+ return self.setGroupProperties(properties)
+
+ security.declareProtected(Permissions.manage_users, 'setGroupProperties')
+ def setGroupProperties(self, mapping):
+ '''Sets the properties of the member.
+ '''
+ # Sets the properties given in the MemberDataTool.
+ tool = self.getTool()
+ for id in tool.propertyIds():
+ if mapping.has_key(id):
+ if not self.__class__.__dict__.has_key(id):
+ value = mapping[id]
+ if type(value)==type(''):
+ proptype = tool.getPropertyType(id) or 'string'
+ if type_converters.has_key(proptype):
+ value = type_converters[proptype](value)
+ setattr(self, id, value)
+
+ # Hopefully we can later make notifyModified() implicit.
+ self.notifyModified()
+
+ security.declarePublic('getProperties')
+ def getProperties(self, ):
+ """ Return the properties of this group. Properties are as usual in Zope."""
+ tool = self.getTool()
+ ret = {}
+ for pty in tool.propertyIds():
+ try:
+ ret[pty] = self.getProperty(pty)
+ except ValueError:
+ # We ignore missing ptys
+ continue
+ return ret
+
+ security.declarePublic('getProperty')
+ def getProperty(self, id, default=_marker):
+ """ Returns the value of the property specified by 'id' """
+ tool = self.getTool()
+ base = aq_base( self )
+
+ # First, check the wrapper (w/o acquisition).
+ value = getattr( base, id, _marker )
+ if value is not _marker:
+ return value
+
+ # Then, check the tool and the user object for a value.
+ tool_value = tool.getProperty( id, _marker )
+ user_value = getattr( aq_base(self.getGroup()), id, _marker )
+
+ # If the tool doesn't have the property, use user_value or default
+ if tool_value is _marker:
+ if user_value is not _marker:
+ return user_value
+ elif default is not _marker:
+ return default
+ else:
+ raise ValueError, 'The property %s does not exist' % id
+
+ # If the tool has an empty property and we have a user_value, use it
+ if not tool_value and user_value is not _marker:
+ return user_value
+
+ # Otherwise return the tool value
+ return tool_value
+
+ def __str__(self):
+ return self.getGroupId()
+
+ security.declarePublic("isGroup")
+ def isGroup(self,):
+ """
+ isGroup(self,) => Return true if this is a group.
+ Will always return true for groups.
+ As MemberData objects do not support this method, it is quite useless by now.
+ So one can use groupstool.isGroup(g) instead to get this information.
+ """
+ return 1
+
+ ### Group object interface ###
+
+ security.declarePublic('getGroupName')
+ def getGroupName(self):
+ """Return the name of the group, without any special decorations (like GRUF prefixes.)"""
+ return self.getGroup().getName()
+
+ security.declarePublic('getGroupId')
+ def getGroupId(self):
+ """Get the ID of the group. The ID can be used, at least from
+ Python, to get the user from the user's UserDatabase.
+ Within Plone, all group ids are UNPREFIXED."""
+ if isinstance(self, GRUFGroup):
+ return self.getGroup().getId(unprefixed = 1)
+ else:
+ return self.getGroup().getId()
+
+ def getGroupTitleOrName(self):
+ """Get the Title property of the group. If there is none
+ then return the name """
+ title = self.getProperty('title', None)
+ return title or self.getGroupName()
+
+ security.declarePublic("getMemberId")
+ def getMemberId(self,):
+ """This exists only for a basic user/group API compatibility
+ """
+ return self.getGroupId()
+
+ security.declarePublic('getRoles')
+ def getRoles(self):
+ """Return the list of roles assigned to a user."""
+ return self.getGroup().getRoles()
+
+ security.declarePublic('getRolesInContext')
+ def getRolesInContext(self, object):
+ """Return the list of roles assigned to the user, including local
+ roles assigned in context of the passed in object."""
+ return self.getGroup().getRolesInContext(object)
+
+ security.declarePublic('getDomains')
+ def getDomains(self):
+ """Return the list of domain restrictions for a user"""
+ return self.getGroup().getDomains()
+
+ security.declarePublic('has_role')
+ def has_role(self, roles, object=None):
+ """Check to see if a user has a given role or roles."""
+ return self.getGroup().has_role(roles, object)
+
+ # There are other parts of the interface but they are
+ # deprecated for use with CMF applications.
+
+InitializeClass(GroupData)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+GroupUserFolder product
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: GroupUserFolder.py 40118 2007-04-01 15:13:44Z alecm $
+__docformat__ = 'restructuredtext'
+
+
+# fakes a method from a DTML file
+from Globals import MessageDialog, DTMLFile
+
+from AccessControl import ClassSecurityInfo
+from AccessControl import Permissions
+from AccessControl import getSecurityManager
+from AccessControl import Unauthorized
+from Globals import InitializeClass
+from Acquisition import aq_base, aq_inner, aq_parent
+from Acquisition import Implicit
+from Globals import Persistent
+from AccessControl.Role import RoleManager
+from OFS.SimpleItem import Item
+from OFS.PropertyManager import PropertyManager
+import OFS
+from OFS import ObjectManager, SimpleItem
+from DateTime import DateTime
+from App import ImageFile
+from Products.PageTemplates import PageTemplateFile
+import AccessControl.Role, webdav.Collection
+import Products
+import os
+import string
+import sys
+import time
+import math
+import random
+from global_symbols import *
+import AccessControl.User
+import GRUFFolder
+import GRUFUser
+from Products.PageTemplates import PageTemplateFile
+import class_utility
+from Products.GroupUserFolder import postonly
+
+from interfaces.IUserFolder import IUserFolder
+
+## Developers notes
+##
+## The REQUEST.GRUF_PROBLEM variable is defined whenever GRUF encounters
+## a problem than can be showed in the management screens. It's always
+## logged as LOG_WARNING level anyway.
+
+_marker = []
+
+def unique(sequence, _list = 0):
+ """Make a sequence a list of unique items"""
+ uniquedict = {}
+ for v in sequence:
+ uniquedict[v] = 1
+ if _list:
+ return list(uniquedict.keys())
+ return tuple(uniquedict.keys())
+
+
+def manage_addGroupUserFolder(self, dtself=None, REQUEST=None, **ignored):
+ """ Factory method that creates a UserFolder"""
+ f=GroupUserFolder()
+ self=self.this()
+ try: self._setObject('acl_users', f)
+ except: return MessageDialog(
+ title ='Item Exists',
+ message='This object already contains a User Folder',
+ action ='%s/manage_main' % REQUEST['URL1'])
+ self.__allow_groups__=f
+ self.acl_users._post_init()
+
+ self.acl_users.Users.manage_addUserFolder()
+ self.acl_users.Groups.manage_addUserFolder()
+
+ if REQUEST is not None:
+ REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
+
+
+
+
+class GroupUserFolder(OFS.ObjectManager.ObjectManager,
+ AccessControl.User.BasicUserFolder,
+ ):
+ """
+ GroupUserFolder => User folder with groups management
+ """
+
+ # #
+ # ZOPE INFORMATION #
+ # #
+
+ meta_type='Group User Folder'
+ id ='acl_users'
+ title ='Group-aware User Folder'
+
+ __implements__ = (IUserFolder, )
+ def __creatable_by_emergency_user__(self): return 1
+
+ isAnObjectManager = 1
+ isPrincipiaFolderish = 1
+ isAUserFolder = 1
+
+## _haveLDAPUF = 0
+
+ security = ClassSecurityInfo()
+
+ manage_options=(
+ (
+ {'label':'Overview', 'action':'manage_overview'},
+ {'label':'Sources', 'action':'manage_GRUFSources'},
+ {'label':'LDAP Wizard', 'action':'manage_wizard'},
+ {'label':'Groups', 'action':'manage_groups'},
+ {'label':'Users', 'action':'manage_users'},
+ {'label':'Audit', 'action':'manage_audit'},
+ ) + \
+ OFS.ObjectManager.ObjectManager.manage_options + \
+ RoleManager.manage_options + \
+ Item.manage_options )
+
+ manage_main = OFS.ObjectManager.ObjectManager.manage_main
+## manage_overview = DTMLFile('dtml/GRUF_overview', globals())
+ manage_overview = PageTemplateFile.PageTemplateFile('dtml/GRUF_overview', globals())
+ manage_audit = PageTemplateFile.PageTemplateFile('dtml/GRUF_audit', globals())
+ manage_wizard = PageTemplateFile.PageTemplateFile('dtml/GRUF_wizard', globals())
+ manage_groups = PageTemplateFile.PageTemplateFile('dtml/GRUF_groups', globals())
+ manage_users = PageTemplateFile.PageTemplateFile('dtml/GRUF_users', globals())
+ manage_newusers = PageTemplateFile.PageTemplateFile('dtml/GRUF_newusers', globals())
+ manage_GRUFSources = PageTemplateFile.PageTemplateFile('dtml/GRUF_contents', globals())
+ manage_user = PageTemplateFile.PageTemplateFile('dtml/GRUF_user', globals())
+
+ __ac_permissions__=(
+ ('Manage users',
+ ('manage_users',
+ 'user_names', 'setDomainAuthenticationMode',
+ )
+ ),
+ )
+
+
+ # Color constants, only useful within GRUF management screens
+ user_color = "#006600"
+ group_color = "#000099"
+ role_color = "#660000"
+
+ # User and group images
+ img_user = ImageFile.ImageFile('www/GRUFUsers.gif', globals())
+ img_group = ImageFile.ImageFile('www/GRUFGroups.gif', globals())
+
+
+
+ # #
+ # OFFICIAL INTERFACE #
+ # #
+
+ security.declarePublic("hasUsers")
+ def hasUsers(self, ):
+ """
+ From Zope 2.7's User.py:
+ This is not a formal API method: it is used only to provide
+ a way for the quickstart page to determine if the default user
+ folder contains any users to provide instructions on how to
+ add a user for newbies. Using getUserNames or getUsers would have
+ posed a denial of service risk.
+ In GRUF, this method always return 1."""
+ return 1
+
+ security.declareProtected(Permissions.manage_users, "user_names")
+ def user_names(self,):
+ """
+ user_names() => return user IDS and not user NAMES !!!
+ Due to a Zope inconsistency, the Role.get_valid_userids return user names
+ and not user ids - which is bad. As GRUF distinguishes names and ids, this
+ will cause it to break, especially in the listLocalRoles form. So we change
+ user_names() behaviour so that it will return ids and not names.
+ """
+ return self.getUserIds()
+
+
+ security.declareProtected(Permissions.manage_users, "getUserNames")
+ def getUserNames(self, __include_groups__ = 1, __include_users__ = 1, __groups_prefixed__ = 0):
+ """
+ Return a list of all possible user atom names in the system.
+ Groups will be returned WITHOUT their prefix by this method.
+ So, there might be a collision between a user name and a group name.
+ [NOTA: This method is time-expensive !]
+ """
+ if __include_users__:
+ LogCallStack(LOG_DEBUG, "This call can be VERY expensive!")
+ names = []
+ ldap_sources = []
+
+ # Fetch users in user sources
+ if __include_users__:
+ for src in self.listUserSources():
+ names.extend(src.getUserNames())
+
+ # Append groups if possible
+ if __include_groups__:
+ # Regular groups
+ if "acl_users" in self._getOb('Groups').objectIds():
+ names.extend(self.Groups.listGroups(prefixed = __groups_prefixed__))
+
+ # LDAP groups
+ for ldapuf in ldap_sources:
+ if ldapuf._local_groups:
+ continue
+ for g in ldapuf.getGroups(attr = LDAP_GROUP_RDN):
+ if __groups_prefixed__:
+ names.append("%s%s" % (GROUP_PREFIX, g))
+ else:
+ names.append(g)
+ # Return a list of unique names
+ return unique(names, _list = 1)
+
+ security.declareProtected(Permissions.manage_users, "getUserIds")
+ def getUserIds(self,):
+ """
+ Return a list of all possible user atom ids in the system.
+ WARNING: Please see the id Vs. name consideration at the
+ top of this document. So, groups will be returned
+ WITH their prefix by this method
+ [NOTA: This method is time-expensive !]
+ """
+ return self.getUserNames(__groups_prefixed__ = 1)
+
+ security.declareProtected(Permissions.manage_users, "getUsers")
+ def getUsers(self, __include_groups__ = 1, __include_users__ = 1):
+ """Return a list of user and group objects.
+ In case of some UF implementations, the returned object may only be a subset
+ of all possible users.
+ In other words, you CANNOT assert that len(getUsers()) equals len(getUserNames()).
+ With cache-support UserFolders, such as LDAPUserFolder, the getUser() method will
+ return only cached user objects instead of fetching all possible users.
+ """
+ Log(LOG_DEBUG, "getUsers")
+ ret = []
+ names_set = {}
+
+ # avoid too many lookups for 'has_key' in loops
+ isUserProcessed = names_set.has_key
+
+ # Fetch groups first (then the user must be
+ # prefixed by 'group_' prefix)
+ if __include_groups__:
+ # Fetch regular groups
+ for u in self._getOb('Groups').acl_users.getUsers():
+ if not u:
+ continue # Ignore empty users
+
+ name = u.getId()
+ if isUserProcessed(name):
+ continue # Prevent double users inclusion
+
+ # Append group
+ names_set[name] = True
+ ret.append(
+ GRUFUser.GRUFGroup(u, self, isGroup = 1, source_id = "Groups").__of__(self)
+ )
+
+ # Fetch users then
+ if __include_users__:
+ for src in self.listUserSources():
+ for u in src.getUsers():
+ if not u:
+ continue # Ignore empty users
+
+ name = u.getId()
+ if isUserProcessed(name):
+ continue # Prevent double users inclusion
+
+ # Append user
+ names_set[name] = True
+ ret.append(
+ GRUFUser.GRUFUser(u, self, source_id = src.getUserSourceId(), isGroup = 0).__of__(self)
+ )
+
+ return tuple(ret)
+
+ security.declareProtected(Permissions.manage_users, "getUser")
+ def getUser(self, name, __include_users__ = 1, __include_groups__ = 1, __force_group_id__ = 0):
+ """
+ Return the named user object or None.
+ User have precedence over group.
+ If name is None, getUser() will return None.
+ """
+ # Basic check
+ if name is None:
+ return None
+
+ # Prevent infinite recursion when instanciating a GRUF
+ # without having sub-acl_users set
+ if not "acl_users" in self._getOb('Groups').objectIds():
+ return None
+
+ # Fetch groups first (then the user must be prefixed by 'group_' prefix)
+ if __include_groups__ and name.startswith(GROUP_PREFIX):
+ id = name[GROUP_PREFIX_LEN:]
+
+ # Fetch regular groups
+ u = self._getOb('Groups')._getGroup(id)
+ if u:
+ ret = GRUFUser.GRUFGroup(
+ u, self, isGroup = 1, source_id = "Groups"
+ ).__of__(self)
+ return ret # XXX This violates precedence
+
+ # Fetch users then
+ if __include_users__:
+ for src in self.listUserSources():
+ u = src.getUser(name)
+ if u:
+ ret = GRUFUser.GRUFUser(u, self, source_id = src.getUserSourceId(), isGroup = 0).__of__(self)
+ return ret
+
+ # Then desperatly try to fetch groups (without beeing prefixed by 'group_' prefix)
+ if __include_groups__ and (not __force_group_id__):
+ u = self._getOb('Groups')._getGroup(name)
+ if u:
+ ret = GRUFUser.GRUFGroup(u, self, isGroup = 1, source_id = "Groups").__of__(self)
+ return ret
+
+ return None
+
+
+ security.declareProtected(Permissions.manage_users, "getUserById")
+ def getUserById(self, id, default=_marker):
+ """Return the user atom corresponding to the given id. Can return groups.
+ """
+ ret = self.getUser(id, __force_group_id__ = 1)
+ if not ret:
+ if default is _marker:
+ return None
+ ret = default
+ return ret
+
+
+ security.declareProtected(Permissions.manage_users, "getUserByName")
+ def getUserByName(self, name, default=_marker):
+ """Same as getUser() but works with a name instead of an id.
+ [NOTA: Theorically, the id is a handle, while the name is the actual login name.
+ But difference between a user id and a user name is unsignificant in
+ all current User Folder implementations... except for GROUPS.]
+ """
+ # Try to fetch a user first
+ usr = self.getUser(name)
+
+ # If not found, try to fetch a group by appending the prefix
+ if not usr:
+ name = "%s%s" % (GROUP_PREFIX, name)
+ usr = self.getUserById(name, default)
+
+ return usr
+
+ security.declareProtected(Permissions.manage_users, "getPureUserNames")
+ def getPureUserNames(self, ):
+ """Fetch the list of actual users from GRUFUsers.
+ """
+ return self.getUserNames(__include_groups__ = 0)
+
+
+ security.declareProtected(Permissions.manage_users, "getPureUserIds")
+ def getPureUserIds(self,):
+ """Same as getUserIds() but without groups
+ """
+ return self.getUserNames(__include_groups__ = 0)
+
+ security.declareProtected(Permissions.manage_users, "getPureUsers")
+ def getPureUsers(self):
+ """Return a list of pure user objects.
+ """
+ return self.getUsers(__include_groups__ = 0)
+
+ security.declareProtected(Permissions.manage_users, "getPureUser")
+ def getPureUser(self, id, ):
+ """Return the named user object or None"""
+ # Performance tricks
+ if not id:
+ return None
+
+ # Fetch it
+ return self.getUser(id, __include_groups__ = 0)
+
+
+ security.declareProtected(Permissions.manage_users, "getGroupNames")
+ def getGroupNames(self, ):
+ """Same as getUserNames() but without pure users.
+ """
+ return self.getUserNames(__include_users__ = 0, __groups_prefixed__ = 0)
+
+ security.declareProtected(Permissions.manage_users, "getGroupIds")
+ def getGroupIds(self, ):
+ """Same as getUserNames() but without pure users.
+ """
+ return self.getUserNames(__include_users__ = 0, __groups_prefixed__ = 1)
+
+ security.declareProtected(Permissions.manage_users, "getGroups")
+ def getGroups(self):
+ """Same as getUsers() but without pure users.
+ """
+ return self.getUsers(__include_users__ = 0)
+
+ security.declareProtected(Permissions.manage_users, "getGroup")
+ def getGroup(self, name, prefixed = 1):
+ """Return the named user object or None"""
+ # Performance tricks
+ if not name:
+ return None
+
+ # Unprefix group name
+ if not name.startswith(GROUP_PREFIX):
+ name = "%s%s" % (GROUP_PREFIX, name, )
+
+ # Fetch it
+ return self.getUser(name, __include_users__ = 0)
+
+ security.declareProtected(Permissions.manage_users, "getGroupById")
+ def getGroupById(self, id, default = _marker):
+ """Same as getUserById(id) but forces returning a group.
+ """
+ ret = self.getUser(id, __include_users__ = 0, __force_group_id__ = 1)
+ if not ret:
+ if default is _marker:
+ return None
+ ret = default
+ return ret
+
+ security.declareProtected(Permissions.manage_users, "getGroupByName")
+ def getGroupByName(self, name, default = _marker):
+ """Same as getUserByName(name) but forces returning a group.
+ """
+ ret = self.getUser(name, __include_users__ = 0, __force_group_id__ = 0)
+ if not ret:
+ if default is _marker:
+ return None
+ ret = default
+ return ret
+
+
+
+ # #
+ # REGULAR MUTATORS #
+ # #
+
+ security.declareProtected(Permissions.manage_users, "userFolderAddUser")
+ def userFolderAddUser(self, name, password, roles, domains, groups = (),
+ REQUEST=None, **kw):
+ """API method for creating a new user object. Note that not all
+ user folder implementations support dynamic creation of user
+ objects.
+ """
+ return self._doAddUser(name, password, roles, domains, groups, **kw)
+ userFolderAddUser = postonly(userFolderAddUser)
+
+ security.declareProtected(Permissions.manage_users, "userFolderEditUser")
+ def userFolderEditUser(self, name, password, roles, domains, groups = None,
+ REQUEST=None, **kw):
+ """API method for changing user object attributes. Note that not
+ all user folder implementations support changing of user object
+ attributes.
+ Arguments ARE required.
+ """
+ return self._doChangeUser(name, password, roles, domains, groups, **kw)
+ userFolderEditUser = postonly(userFolderEditUser)
+
+ security.declareProtected(Permissions.manage_users, "userFolderUpdateUser")
+ def userFolderUpdateUser(self, name, password = None, roles = None,
+ domains = None, groups = None, REQUEST=None, **kw):
+ """API method for changing user object attributes. Note that not
+ all user folder implementations support changing of user object
+ attributes.
+ Arguments are optional"""
+ return self._updateUser(name, password, roles, domains, groups, **kw)
+ userFolderUpdateUser = postonly(userFolderUpdateUser)
+
+ security.declareProtected(Permissions.manage_users, "userFolderDelUsers")
+ def userFolderDelUsers(self, names, REQUEST=None):
+ """API method for deleting one or more user atom objects. Note that not
+ all user folder implementations support deletion of user objects."""
+ return self._doDelUsers(names)
+ userFolderDelUsers = postonly(userFolderDelUsers)
+
+ security.declareProtected(Permissions.manage_users, "userFolderAddGroup")
+ def userFolderAddGroup(self, name, roles, groups = (), REQUEST=None, **kw):
+ """API method for creating a new group.
+ """
+ while name.startswith(GROUP_PREFIX):
+ name = name[GROUP_PREFIX_LEN:]
+ return self._doAddGroup(name, roles, groups, **kw)
+ userFolderAddGroup = postonly(userFolderAddGroup)
+
+ security.declareProtected(Permissions.manage_users, "userFolderEditGroup")
+ def userFolderEditGroup(self, name, roles, groups = None, REQUEST=None,
+ **kw):
+ """API method for changing group object attributes.
+ """
+ return self._doChangeGroup(name, roles = roles, groups = groups, **kw)
+ userFolderEditGroup = postonly(userFolderEditGroup)
+
+ security.declareProtected(Permissions.manage_users, "userFolderUpdateGroup")
+ def userFolderUpdateGroup(self, name, roles = None, groups = None,
+ REQUEST=None, **kw):
+ """API method for changing group object attributes.
+ """
+ return self._updateGroup(name, roles = roles, groups = groups, **kw)
+ userFolderUpdateGroup = postonly(userFolderUpdateGroup)
+
+ security.declareProtected(Permissions.manage_users, "userFolderDelGroups")
+ def userFolderDelGroups(self, names, REQUEST=None):
+ """API method for deleting one or more group objects.
+ Implem. note : All ids must be prefixed with 'group_',
+ so this method ends up beeing only a filter of non-prefixed ids
+ before calling userFolderDelUsers().
+ """
+ return self._doDelGroups(names)
+ userFolderDelUsers = postonly(userFolderDelUsers)
+
+
+
+ # #
+ # SEARCH METHODS #
+ # #
+
+
+ security.declareProtected(Permissions.manage_users, "searchUsersByAttribute")
+ def searchUsersByAttribute(self, attribute, search_term):
+ """Return user ids whose 'attribute' match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying user folder:
+ it may return all users, return only cached users (for LDAPUF) or return no users.
+ This will return all users whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON USER FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF).
+ 'attribute' can be 'id' or 'name' for all UF kinds, or anything else for LDAPUF.
+ """
+ ret = []
+ for src in self.listUserSources():
+ # Use source-specific search methods if available
+ if hasattr(src.aq_base, "findUser"):
+ # LDAPUF
+ Log(LOG_DEBUG, "We use LDAPUF to find users")
+ id_attr = src._uid_attr
+ if attribute == 'name':
+ attr = src._login_attr
+ elif attribute == 'id':
+ attr = src._uid_attr
+ else:
+ attr = attribute
+ Log(LOG_DEBUG, "we use findUser", attr, search_term, )
+ users = src.findUser(attr, search_term, exact_match = True)
+ ret.extend(
+ [ u[id_attr] for u in users ],
+ )
+ else:
+ # Other types of user folder
+ search_term = search_term.lower()
+
+ # Find the proper method according to the attribute type
+ if attribute == "name":
+ method = "getName"
+ elif attribute == "id":
+ method = "getId"
+ else:
+ raise NotImplementedError, "Attribute searching is only supported for LDAPUserFolder by now."
+
+ # Actually search
+ src_id = src.getUserSourceId()
+ for u in src.getUsers():
+ if not u:
+ continue
+ u = GRUFUser.GRUFUser(u, self, source_id=src_id,
+ isGroup=0).__of__(self)
+ s = getattr(u, method)().lower()
+ if string.find(s, search_term) != -1:
+ ret.append(u.getId())
+ Log(LOG_DEBUG, "We've found them:", ret)
+ return ret
+
+ security.declareProtected(Permissions.manage_users, "searchUsersByName")
+ def searchUsersByName(self, search_term):
+ """Return user ids whose name match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying user folder:
+ it may return all users, return only cached users (for LDAPUF) or return no users.
+ This will return all users whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON USER FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF)
+ """
+ return self.searchUsersByAttribute("name", search_term)
+
+ security.declareProtected(Permissions.manage_users, "searchUsersById")
+ def searchUsersById(self, search_term):
+ """Return user ids whose id match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying user folder:
+ it may return all users, return only cached users (for LDAPUF) or return no users.
+ This will return all users whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON USER FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF)
+ """
+ return self.searchUsersByAttribute("id", search_term)
+
+
+ security.declareProtected(Permissions.manage_users, "searchGroupsByAttribute")
+ def searchGroupsByAttribute(self, attribute, search_term):
+ """Return group ids whose 'attribute' match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying group folder:
+ it may return all groups, return only cached groups (for LDAPUF) or return no groups.
+ This will return all groups whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON GROUP FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF).
+ 'attribute' can be 'id' or 'name' for all UF kinds, or anything else for LDAPUF.
+ """
+ ret = []
+ src = self.Groups
+
+ # Use source-specific search methods if available
+ if hasattr(src.aq_base, "findGroup"):
+ # LDAPUF
+ id_attr = src._uid_attr
+ if attribute == 'name':
+ attr = src._login_attr
+ elif attribute == 'id':
+ attr = src._uid_attr
+ else:
+ attr = attribute
+ groups = src.findGroup(attr, search_term)
+ ret.extend(
+ [ u[id_attr] for u in groups ],
+ )
+ else:
+ # Other types of group folder
+ search_term = search_term.lower()
+
+ # Find the proper method according to the attribute type
+ if attribute == "name":
+ method = "getName"
+ elif attribute == "id":
+ method = "getId"
+ else:
+ raise NotImplementedError, "Attribute searching is only supported for LDAPGroupFolder by now."
+
+ # Actually search
+ for u in self.getGroups():
+ s = getattr(u, method)().lower()
+ if string.find(s, search_term) != -1:
+ ret.append(u.getId())
+ return ret
+
+ security.declareProtected(Permissions.manage_users, "searchGroupsByName")
+ def searchGroupsByName(self, search_term):
+ """Return group ids whose name match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying group folder:
+ it may return all groups, return only cached groups (for LDAPUF) or return no groups.
+ This will return all groups whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON GROUP FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF)
+ """
+ return self.searchGroupsByAttribute("name", search_term)
+
+ security.declareProtected(Permissions.manage_users, "searchGroupsById")
+ def searchGroupsById(self, search_term):
+ """Return group ids whose id match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying group folder:
+ it may return all groups, return only cached groups (for LDAPUF) or return no groups.
+ This will return all groups whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON GROUP FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF)
+ """
+ return self.searchGroupsByAttribute("id", search_term)
+
+ # #
+ # SECURITY MANAGEMENT METHODS #
+ # #
+
+ security.declareProtected(Permissions.manage_users, "setRolesOnUsers")
+ def setRolesOnUsers(self, roles, userids, REQUEST = None):
+ """Set a common set of roles for a bunch of user atoms.
+ """
+ for usr in userids:
+ self.userSetRoles(usr, roles)
+ setRolesOnUsers = postonly(setRolesOnUsers)
+
+## def setUsersOfRole(self, usernames, role):
+## """Sets the users of a role.
+## XXX THIS METHOD SEEMS TO BE SEAMLESS.
+## """
+## raise NotImplementedError, "Not implemented."
+
+ security.declareProtected(Permissions.manage_users, "getUsersOfRole")
+ def getUsersOfRole(self, role, object = None):
+ """Gets the user (and group) ids having the specified role...
+ ...on the specified Zope object if it's not None
+ ...on their own information if the object is None.
+ NOTA: THIS METHOD IS VERY EXPENSIVE.
+ XXX PERFORMANCES HAVE TO BE IMPROVED
+ """
+ ret = []
+ for id in self.getUserIds():
+ if role in self.getRolesOfUser(id):
+ ret.append(id)
+ return tuple(ret)
+
+ security.declarePublic("getRolesOfUser")
+ def getRolesOfUser(self, userid):
+ """Alias for user.getRoles()
+ """
+ return self.getUserById(userid).getRoles()
+
+ security.declareProtected(Permissions.manage_users, "userFolderAddRole")
+ def userFolderAddRole(self, role, REQUEST=None):
+ """Add a new role. The role will be appended, in fact, in GRUF's surrounding folder.
+ """
+ if role in self.aq_parent.valid_roles():
+ raise ValueError, "Role '%s' already exist" % (role, )
+
+ return self.aq_parent._addRole(role)
+ userFolderAddRole = postonly(userFolderAddRole)
+
+ security.declareProtected(Permissions.manage_users, "userFolderDelRoles")
+ def userFolderDelRoles(self, roles, REQUEST=None):
+ """Delete roles.
+ The removed roles will be removed from the UserFolder's users and groups as well,
+ so this method can be very time consuming with a large number of users.
+ """
+ # Check that roles exist
+ ud_roles = self.aq_parent.userdefined_roles()
+ for r in roles:
+ if not r in ud_roles:
+ raise ValueError, "Role '%s' is not defined on acl_users' parent folder" % (r, )
+
+ # Remove role on all users
+ for r in roles:
+ for u in self.getUsersOfRole(r, ):
+ self.userRemoveRole(u, r, )
+
+ # Actually remove role
+ return self.aq_parent._delRoles(roles, None)
+ userFolderDelRoles = postonly(userFolderDelRoles)
+
+ security.declarePublic("userFolderGetRoles")
+ def userFolderGetRoles(self, ):
+ """
+ userFolderGetRoles(self,) => tuple of strings
+ List the roles defined at the top of GRUF's folder.
+ This includes both user-defined roles and default roles.
+ """
+ return tuple(self.aq_parent.valid_roles())
+
+
+ # Groups support
+
+ security.declareProtected(Permissions.manage_users, "setMembers")
+ def setMembers(self, groupid, userids, REQUEST=None):
+ """Set the members of the group
+ """
+ self.getGroup(groupid).setMembers(userids)
+ setMembers = postonly(setMembers)
+
+ security.declareProtected(Permissions.manage_users, "addMember")
+ def addMember(self, groupid, userid, REQUEST=None):
+ """Add a member to a group
+ """
+ return self.getGroup(groupid).addMember(userid)
+ addMember = postonly(addMember)
+
+ security.declareProtected(Permissions.manage_users, "removeMember")
+ def removeMember(self, groupid, userid, REQUEST=None):
+ """Remove a member from a group.
+ """
+ return self.getGroup(groupid).removeMember(userid)
+ removeMember = postonly(removeMember)
+
+ security.declareProtected(Permissions.manage_users, "getMemberIds")
+ def getMemberIds(self, groupid):
+ """Return the list of member ids (groups and users) in this group
+ """
+ m = self.getGroup(groupid)
+ if not m:
+ raise ValueError, "Invalid group: '%s'" % groupid
+ return self.getGroup(groupid).getMemberIds()
+
+ security.declareProtected(Permissions.manage_users, "getUserMemberIds")
+ def getUserMemberIds(self, groupid):
+ """Return the list of member ids (groups and users) in this group
+ """
+ return self.getGroup(groupid).getUserMemberIds()
+
+ security.declareProtected(Permissions.manage_users, "getGroupMemberIds")
+ def getGroupMemberIds(self, groupid):
+ """Return the list of member ids (groups and users) in this group
+ XXX THIS MAY BE VERY EXPENSIVE !
+ """
+ return self.getGroup(groupid).getGroupMemberIds()
+
+ security.declareProtected(Permissions.manage_users, "hasMember")
+ def hasMember(self, groupid, id):
+ """Return true if the specified atom id is in the group.
+ This is the contrary of IUserAtom.isInGroup(groupid).
+ THIS CAN BE VERY EXPENSIVE
+ """
+ return self.getGroup(groupid).hasMember(id)
+
+
+ # User mutation
+
+## def setUserId(id, newId):
+## """Change id of a user atom.
+## """
+
+## def setUserName(id, newName):
+## """Change the name of a user atom.
+## """
+
+ security.declareProtected(Permissions.manage_users, "userSetRoles")
+ def userSetRoles(self, id, roles, REQUEST=None):
+ """Change the roles of a user atom.
+ """
+ self._updateUser(id, roles = roles)
+ userSetRoles = postonly(userSetRoles)
+
+ security.declareProtected(Permissions.manage_users, "userAddRole")
+ def userAddRole(self, id, role, REQUEST=None):
+ """Append a role for a user atom
+ """
+ roles = list(self.getUser(id).getRoles())
+ if not role in roles:
+ roles.append(role)
+ self._updateUser(id, roles = roles)
+ userAddRole = postonly(userAddRole)
+
+ security.declareProtected(Permissions.manage_users, "userRemoveRole")
+ def userRemoveRole(self, id, role, REQUEST=None):
+ """Remove the role of a user atom. Will NOT complain if role doesn't exist
+ """
+ roles = list(self.getRolesOfUser(id))
+ if role in roles:
+ roles.remove(role)
+ self._updateUser(id, roles = roles)
+ userRemoveRole = postonly(userRemoveRole)
+
+ security.declareProtected(Permissions.manage_users, "userSetPassword")
+ def userSetPassword(self, id, newPassword, REQUEST=None):
+ """Set the password of a user
+ """
+ u = self.getPureUser(id)
+ if not u:
+ raise ValueError, "Invalid pure user id: '%s'" % (id,)
+ self._updateUser(u.getId(), password = newPassword, )
+ userSetPassword = postonly(userSetPassword)
+
+ security.declareProtected(Permissions.manage_users, "userGetDomains")
+ def userGetDomains(self, id):
+ """get domains for a user
+ """
+ usr = self.getPureUser(id)
+ return tuple(usr.getDomains())
+
+ security.declareProtected(Permissions.manage_users, "userSetDomains")
+ def userSetDomains(self, id, domains, REQUEST=None):
+ """Set domains for a user
+ """
+ usr = self.getPureUser(id)
+ self._updateUser(usr.getId(), domains = domains, )
+ userSetDomains = postonly(userSetDomains)
+
+ security.declareProtected(Permissions.manage_users, "userAddDomain")
+ def userAddDomain(self, id, domain, REQUEST=None):
+ """Append a domain to a user
+ """
+ usr = self.getPureUser(id)
+ domains = list(usr.getDomains())
+ if not domain in domains:
+ roles.append(domain)
+ self._updateUser(usr.getId(), domains = domains, )
+ userAddDomain = postonly(userAddDomain)
+
+ security.declareProtected(Permissions.manage_users, "userRemoveDomain")
+ def userRemoveDomain(self, id, domain, REQUEST=None):
+ """Remove a domain from a user
+ """
+ usr = self.getPureUser(id)
+ domains = list(usr.getDomains())
+ if not domain in domains:
+ raise ValueError, "User '%s' doesn't have domain '%s'" % (id, domain, )
+ while domain in domains:
+ roles.remove(domain)
+ self._updateUser(usr.getId(), domains = domains)
+ userRemoveDomain = postonly(userRemoveDomain)
+
+ security.declareProtected(Permissions.manage_users, "userSetGroups")
+ def userSetGroups(self, id, groupnames, REQUEST=None):
+ """Set the groups of a user
+ """
+ self._updateUser(id, groups = groupnames)
+ userSetGroups = postonly(userSetGroups)
+
+ security.declareProtected(Permissions.manage_users, "userAddGroup")
+ def userAddGroup(self, id, groupname, REQUEST=None):
+ """add a group to a user atom
+ """
+ groups = list(self.getUserById(id).getGroups())
+ if not groupname in groups:
+ groups.append(groupname)
+ self._updateUser(id, groups = groups)
+ userAddGroup = postonly(userAddGroup)
+
+
+ security.declareProtected(Permissions.manage_users, "userRemoveGroup")
+ def userRemoveGroup(self, id, groupname, REQUEST=None):
+ """remove a group from a user atom.
+ """
+ groups = list(self.getUserById(id).getGroupNames())
+ if groupname.startswith(GROUP_PREFIX):
+ groupname = groupname[GROUP_PREFIX_LEN:]
+ if groupname in groups:
+ groups.remove(groupname)
+ self._updateUser(id, groups = groups)
+ userRemoveGroup = postonly(userRemoveGroup)
+
+
+ # #
+ # VARIOUS OPERATIONS #
+ # #
+
+ def __init__(self):
+ """
+ __init__(self) -> initialization method
+ We define it to prevend calling ancestor's __init__ methods.
+ """
+ pass
+
+
+ security.declarePrivate('_post_init')
+ def _post_init(self):
+ """
+ _post_init(self) => meant to be called when the
+ object is in the Zope tree
+ """
+ uf = GRUFFolder.GRUFUsers()
+ gf = GRUFFolder.GRUFGroups()
+ self._setObject('Users', uf)
+ self._setObject('Groups', gf)
+ self.id = "acl_users"
+
+ def manage_beforeDelete(self, item, container):
+ """
+ Special overloading for __allow_groups__ attribute
+ """
+ if item is self:
+ try:
+ del container.__allow_groups__
+ except:
+ pass
+
+ def manage_afterAdd(self, item, container):
+ """Same
+ """
+ if item is self:
+ container.__allow_groups__ = aq_base(self)
+
+ # #
+ # VARIOUS UTILITIES #
+ # #
+ # These methods shouldn't be used directly for most applications, #
+ # but they might be useful for some special processing. #
+ # #
+
+ security.declarePublic('getGroupPrefix')
+ def getGroupPrefix(self):
+ """ group prefix """
+ return GROUP_PREFIX
+
+ security.declarePrivate('getGRUFPhysicalRoot')
+ def getGRUFPhysicalRoot(self,):
+ # $$$ trick meant to be used within
+ # fake_getPhysicalRoot (see __init__)
+ return self.getPhysicalRoot()
+
+ security.declareProtected(Permissions.view, 'getGRUFId')
+ def getGRUFId(self,):
+ """
+ Alias to self.getId()
+ """
+ return self.getId()
+
+ security.declareProtected(Permissions.manage_users, "getUnwrappedUser")
+ def getUnwrappedUser(self, name):
+ """
+ getUnwrappedUser(self, name) => user object or None
+
+ This method is used to get a User object directly from the User's
+ folder acl_users, without wrapping it with group information.
+
+ This is useful for UserFolders that define additional User classes,
+ when you want to call specific methods on these user objects.
+
+ For example, LDAPUserFolder defines a 'getProperty' method that's
+ not inherited from the standard User object. You can, then, use
+ the getUnwrappedUser() to get the matching user and call this
+ method.
+ """
+ src_id = self.getUser(name).getUserSourceId()
+ return self.getUserSource(src_id).getUser(name)
+
+ security.declareProtected(Permissions.manage_users, "getUnwrappedGroup")
+ def getUnwrappedGroup(self, name):
+ """
+ getUnwrappedGroup(self, name) => user object or None
+
+ Same as getUnwrappedUser but for groups.
+ """
+ return self.Groups.acl_users.getUser(name)
+
+ # #
+ # AUTHENTICATION INTERFACE #
+ # #
+
+ security.declarePrivate("authenticate")
+ def authenticate(self, name, password, request):
+ """
+ Pass the request along to the underlying user-related UserFolder
+ object
+ THIS METHOD RETURNS A USER OBJECT OR NONE, as specified in the code
+ in AccessControl/User.py.
+ We also check for inituser in there.
+ """
+ # Emergency user checking stuff
+ emergency = self._emergency_user
+ if emergency and name == emergency.getUserName():
+ if emergency.authenticate(password, request):
+ return emergency
+ else:
+ return None
+
+ # Usual GRUF authentication
+ for src in self.listUserSources():
+ # XXX We can imagine putting a try/except here to "ignore"
+ # UF errors such as SQL or LDAP shutdown
+ u = src.authenticate(name, password, request)
+ if u:
+ return GRUFUser.GRUFUser(u, self, isGroup = 0, source_id = src.getUserSourceId()).__of__(self)
+
+ # No acl_users in the Users folder or no user authenticated
+ # => we refuse authentication
+ return None
+
+
+
+
+ # #
+ # GRUF'S GUTS :-) #
+ # #
+
+ security.declarePrivate("_doAddUser")
+ def _doAddUser(self, name, password, roles, domains, groups = (), **kw):
+ """
+ Create a new user. This should be implemented by subclasses to
+ do the actual adding of a user. The 'password' will be the
+ original input password, unencrypted. The implementation of this
+ method is responsible for performing any needed encryption.
+ """
+ prefix = GROUP_PREFIX
+
+ # Prepare groups
+ roles = list(roles)
+ gruf_groups = self.getGroupIds()
+ for group in groups:
+ if not group.startswith(prefix):
+ group = "%s%s" % (prefix, group, )
+ if not group in gruf_groups:
+ raise ValueError, "Invalid group: '%s'" % (group, )
+ roles.append(group)
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Really add users
+ return self.getDefaultUserSource()._doAddUser(
+ name,
+ password,
+ roles,
+ domains,
+ **kw)
+
+ security.declarePrivate("_doChangeUser")
+ def _doChangeUser(self, name, password, roles, domains, groups = None, **kw):
+ """
+ Modify an existing user. This should be implemented by subclasses
+ to make the actual changes to a user. The 'password' will be the
+ original input password, unencrypted. The implementation of this
+ method is responsible for performing any needed encryption.
+
+ A None password should not change it (well, we hope so)
+ """
+ # Get actual user name and id
+ usr = self.getUser(name)
+ if usr is None:
+ raise ValueError, "Invalid user: '%s'" % (name,)
+ id = usr.getRealId()
+
+ # Don't lose existing groups
+ if groups is None:
+ groups = usr.getGroups()
+
+ roles = list(roles)
+ groups = list(groups)
+
+ # Change groups affectation
+ cur_groups = self.getGroups()
+ given_roles = tuple(usr.getRoles()) + tuple(roles)
+ for group in groups:
+ if not group.startswith(GROUP_PREFIX, ):
+ group = "%s%s" % (GROUP_PREFIX, group, )
+ if not group in cur_groups and not group in given_roles:
+ roles.append(group)
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Change the user itself
+ src = usr.getUserSourceId()
+ Log(LOG_NOTICE, name, "Source:", src)
+ ret = self.getUserSource(src)._doChangeUser(
+ id, password, roles, domains, **kw)
+
+ # Invalidate user cache if necessary
+ usr.clearCachedGroupsAndRoles()
+ authenticated = getSecurityManager().getUser()
+ if id == authenticated.getId() and hasattr(authenticated, 'clearCachedGroupsAndRoles'):
+ authenticated.clearCachedGroupsAndRoles(self.getUserSource(src).getUser(id))
+
+ return ret
+
+ security.declarePrivate("_updateUser")
+ def _updateUser(self, id, password = None, roles = None, domains = None, groups = None):
+ """
+ _updateUser(self, id, password = None, roles = None, domains = None, groups = None)
+
+ This one should work for users AND groups.
+
+ Front-end to _doChangeUser, but with a better default value support.
+ We guarantee that None values will let the underlying UF keep the original ones.
+ This is not true for the password: some buggy UF implementation may not
+ handle None password correctly :-(
+ """
+ # Get the former values if necessary. Username must be valid !
+ usr = self.getUser(id)
+ if roles is None:
+ # Remove invalid roles and group names
+ roles = usr._original_roles
+ roles = filter(lambda x: not x.startswith(GROUP_PREFIX), roles)
+ roles = filter(lambda x: x not in ('Anonymous', 'Authenticated', 'Shared', ''), roles)
+ else:
+ # Check if roles are valid
+ roles = filter(lambda x: x not in ('Anonymous', 'Authenticated', 'Shared', ''), roles)
+ vr = self.userFolderGetRoles()
+ for r in roles:
+ if not r in vr:
+ raise ValueError, "Invalid or inexistant role: '%s'." % (r, )
+ if domains is None:
+ domains = usr._original_domains
+ if groups is None:
+ groups = usr.getGroups(no_recurse = 1)
+ else:
+ # Check if given groups are valid
+ glist = self.getGroupNames()
+ glist.extend(map(lambda x: "%s%s" % (GROUP_PREFIX, x), glist))
+ for g in groups:
+ if not g in glist:
+ raise ValueError, "Invalid group: '%s'" % (g, )
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Change the user
+ return self._doChangeUser(id, password, roles, domains, groups)
+
+ security.declarePrivate("_doDelUsers")
+ def _doDelUsers(self, names):
+ """
+ Delete one or more users. This should be implemented by subclasses
+ to do the actual deleting of users.
+ This won't delete groups !
+ """
+ # Collect information about user sources
+ sources = {}
+ for name in names:
+ usr = self.getUser(name, __include_groups__ = 0)
+ if not usr:
+ continue # Ignore invalid user names
+ src = usr.getUserSourceId()
+ if not sources.has_key(src):
+ sources[src] = []
+ sources[src].append(name)
+ for src, names in sources.items():
+ self.getUserSource(src)._doDelUsers(names)
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+
+ # #
+ # Groups interface #
+ # #
+
+ security.declarePrivate("_doAddGroup")
+ def _doAddGroup(self, name, roles, groups = (), **kw):
+ """
+ Create a new group. Password will be randomly created, and domain will be None.
+ Supports nested groups.
+ """
+ # Prepare initial data
+ domains = ()
+ password = ""
+ if roles is None:
+ roles = []
+ if groups is None:
+ groups = []
+
+ for x in range(0, 10): # Password will be 10 chars long
+ password = "%s%s" % (password, random.choice(string.lowercase), )
+
+ # Compute roles
+ roles = list(roles)
+ prefix = GROUP_PREFIX
+ gruf_groups = self.getGroupIds()
+ for group in groups:
+ if not group.startswith(prefix):
+ group = "%s%s" % (prefix, group, )
+ if group == "%s%s" % (prefix, name, ):
+ raise ValueError, "Infinite recursion for group '%s'." % (group, )
+ if not group in gruf_groups:
+ raise ValueError, "Invalid group: '%s' (defined groups are %s)" % (group, gruf_groups)
+ roles.append(group)
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Actual creation
+ return self.Groups.acl_users._doAddUser(
+ name, password, roles, domains, **kw
+ )
+
+ security.declarePrivate("_doChangeGroup")
+ def _doChangeGroup(self, name, roles, groups = None, **kw):
+ """Modify an existing group."""
+ # Remove prefix if given
+ if name.startswith(self.getGroupPrefix()):
+ name = name[GROUP_PREFIX_LEN:]
+
+ # Check if group exists
+ grp = self.getGroup(name, prefixed = 0)
+ if grp is None:
+ raise ValueError, "Invalid group: '%s'" % (name,)
+
+ # Don't lose existing groups
+ if groups is None:
+ groups = grp.getGroups()
+
+ roles = list(roles or [])
+ groups = list(groups or [])
+
+ # Change groups affectation
+ cur_groups = self.getGroups()
+ given_roles = tuple(grp.getRoles()) + tuple(roles)
+ for group in groups:
+ if not group.startswith(GROUP_PREFIX, ):
+ group = "%s%s" % (GROUP_PREFIX, group, )
+ if group == "%s%s" % (GROUP_PREFIX, grp.id):
+ raise ValueError, "Cannot affect group '%s' to itself!" % (name, ) # Prevent direct inclusion of self
+ new_grp = self.getGroup(group)
+ if not new_grp:
+ raise ValueError, "Invalid or inexistant group: '%s'" % (group, )
+ if "%s%s" % (GROUP_PREFIX, grp.id) in new_grp.getGroups():
+ raise ValueError, "Cannot affect %s to group '%s' as it would lead to circular references." % (group, name, ) # Prevent indirect inclusion of self
+ if not group in cur_groups and not group in given_roles:
+ roles.append(group)
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Perform the change
+ domains = ""
+ password = ""
+ for x in range(0, 10): # Password will be 10 chars long
+ password = "%s%s" % (password, random.choice(string.lowercase), )
+ return self.Groups.acl_users._doChangeUser(name, password,
+ roles, domains, **kw)
+
+ security.declarePrivate("_updateGroup")
+ def _updateGroup(self, name, roles = None, groups = None):
+ """
+ _updateGroup(self, name, roles = None, groups = None)
+
+ Front-end to _doChangeUser, but with a better default value support.
+ We guarantee that None values will let the underlying UF keep the original ones.
+ This is not true for the password: some buggy UF implementation may not
+ handle None password correctly but we do not care for Groups.
+
+ group name can be prefixed or not
+ """
+ # Remove prefix if given
+ if name.startswith(self.getGroupPrefix()):
+ name = name[GROUP_PREFIX_LEN:]
+
+ # Get the former values if necessary. Username must be valid !
+ usr = self.getGroup(name, prefixed = 0)
+ if roles is None:
+ # Remove invalid roles and group names
+ roles = usr._original_roles
+ roles = filter(lambda x: not x.startswith(GROUP_PREFIX), roles)
+ roles = filter(lambda x: x not in ('Anonymous', 'Authenticated', 'Shared'), roles)
+ if groups is None:
+ groups = usr.getGroups(no_recurse = 1)
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Change the user
+ return self._doChangeGroup(name, roles, groups)
+
+
+ security.declarePrivate("_doDelGroup")
+ def _doDelGroup(self, name):
+ """Delete one user."""
+ # Remove prefix if given
+ if name.startswith(self.getGroupPrefix()):
+ name = name[GROUP_PREFIX_LEN:]
+
+ # Reset the users overview batch
+ self._v_batch_users = []
+
+ # Delete it
+ return self.Groups.acl_users._doDelUsers([name])
+
+ security.declarePrivate("_doDelGroups")
+ def _doDelGroups(self, names):
+ """Delete one or more users."""
+ for group in names:
+ if not self.getGroupByName(group, None):
+ continue # Ignore invalid groups
+ self._doDelGroup(group)
+
+
+
+
+ # #
+ # Pretty Management form methods #
+ # #
+
+
+ security.declarePublic('getGRUFVersion')
+ def getGRUFVersion(self,):
+ """
+ getGRUFVersion(self,) => Return human-readable GRUF version as a string.
+ """
+ rev_date = "$Date: 2007-04-01 17:13:44 +0200 (dim, 01 avr 2007) $"[7:-2]
+ return "%s / Revised %s" % (version__, rev_date)
+
+
+ reset_entry = "__None__" # Special entry used for reset
+
+ security.declareProtected(Permissions.manage_users, "changeUser")
+ def changeUser(self, user, groups = [], roles = [], REQUEST = {}, ):
+ """
+ changeUser(self, user, groups = [], roles = [], REQUEST = {}, ) => used in ZMI
+ """
+ obj = self.getUser(user)
+ if obj.isGroup():
+ self._updateGroup(name = user, groups = groups, roles = roles, )
+ else:
+ self._updateUser(id = user, groups = groups, roles = roles, )
+
+
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + "/" + obj.getId() + "/manage_workspace?FORCE_USER=1")
+ changeUser = postonly(changeUser)
+
+ security.declareProtected(Permissions.manage_users, "deleteUser")
+ def deleteUser(self, user, REQUEST = {}, ):
+ """
+ deleteUser(self, user, REQUEST = {}, ) => used in ZMI
+ """
+ pass
+ deleteUser = postonly(deleteUser)
+
+ security.declareProtected(Permissions.manage_users, "changeOrCreateUsers")
+ def changeOrCreateUsers(self, users = [], groups = [], roles = [], new_users = [], default_password = '', REQUEST = {}, ):
+ """
+ changeOrCreateUsers => affect roles & groups to users and/or create new users
+
+ All parameters are strings or lists (NOT tuples !).
+ NO CHECKING IS DONE. This is an utility method, it's not part of the official API.
+ """
+ # Manage roles / groups deletion
+ del_roles = 0
+ del_groups = 0
+ if self.reset_entry in roles:
+ roles.remove(self.reset_entry)
+ del_roles = 1
+ if self.reset_entry in groups:
+ groups.remove(self.reset_entry)
+ del_groups = 1
+ if not roles and not del_roles:
+ roles = None # None instead of [] to avoid deletion
+ add_roles = []
+ else:
+ add_roles = roles
+ if not groups and not del_groups:
+ groups = None
+ add_groups = []
+ else:
+ add_groups = groups
+
+ # Passwords management
+ passwords_list = []
+
+ # Create brand new users
+ for new in new_users:
+ # Strip name
+ name = string.strip(new)
+ if not name:
+ continue
+
+ # Avoid erasing former users
+ if name in map(lambda x: x.getId(), self.getUsers()):
+ continue
+
+ # Use default password or generate a random one
+ if default_password:
+ password = default_password
+ else:
+ password = ""
+ for x in range(0, 8): # Password will be 8 chars long
+ password = "%s%s" % (password, random.choice("ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789"), )
+ self._doAddUser(name, password, add_roles, (), add_groups, )
+
+ # Store the newly created password
+ passwords_list.append({'name':name, 'password':password})
+
+ # Update existing users
+ for user in users:
+ self._updateUser(id = user, groups = groups, roles = roles, )
+
+ # Web request
+ if REQUEST.has_key('RESPONSE'):
+ # Redirect if no users have been created
+ if not passwords_list:
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + "/manage_users")
+
+ # Show passwords form
+ else:
+ REQUEST.set('USER_PASSWORDS', passwords_list)
+ return self.manage_newusers(None, self)
+
+ # Simply return the list of created passwords
+ return passwords_list
+ changeOrCreateUsers = postonly(changeOrCreateUsers)
+
+ security.declareProtected(Permissions.manage_users, "deleteUsers")
+ def deleteUsers(self, users = [], REQUEST = {}):
+ """
+ deleteUsers => explicit
+
+ All parameters are strings. NO CHECKING IS DONE. This is an utility method !
+ """
+ # Delete them
+ self._doDelUsers(users, )
+
+ # Redirect
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + "/manage_users")
+ deleteUsers = postonly(deleteUsers)
+
+ security.declareProtected(Permissions.manage_users, "changeOrCreateGroups")
+ def changeOrCreateGroups(self, groups = [], roles = [], nested_groups = [], new_groups = [], REQUEST = {}, ):
+ """
+ changeOrCreateGroups => affect roles to groups and/or create new groups
+
+ All parameters are strings. NO CHECKING IS DONE. This is an utility method !
+ """
+ # Manage roles / groups deletion
+ del_roles = 0
+ del_groups = 0
+ if self.reset_entry in roles:
+ roles.remove(self.reset_entry)
+ del_roles = 1
+ if self.reset_entry in nested_groups:
+ nested_groups.remove(self.reset_entry)
+ del_groups = 1
+ if not roles and not del_roles:
+ roles = None # None instead of [] to avoid deletion
+ add_roles = []
+ else:
+ add_roles = roles
+ if not nested_groups and not del_groups:
+ nested_groups = None
+ add_groups = []
+ else:
+ add_groups = nested_groups
+
+ # Create brand new groups
+ for new in new_groups:
+ name = string.strip(new)
+ if not name:
+ continue
+ self._doAddGroup(name, roles, groups = add_groups)
+
+ # Update existing groups
+ for group in groups:
+ self._updateGroup(group, roles = roles, groups = nested_groups)
+
+ # Redirect
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + "/manage_groups")
+ changeOrCreateGroups = postonly(changeOrCreateGroups)
+
+ security.declareProtected(Permissions.manage_users, "deleteGroups")
+ def deleteGroups(self, groups = [], REQUEST = {}):
+ """
+ deleteGroups => explicit
+
+ All parameters are strings. NO CHECKING IS DONE. This is an utility method !
+ """
+ # Delete groups
+ for group in groups:
+ self._doDelGroup(group, )
+
+ # Redirect
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + "/manage_groups")
+ deleteGroups = postonly(deleteGroups)
+
+ # #
+ # Local Roles Acquisition Blocking #
+ # Those two methods perform their own security check. #
+ # #
+
+ security.declarePublic("acquireLocalRoles")
+ def acquireLocalRoles(self, folder, status, REQUEST=None):
+ """
+ Enable or disable local role acquisition on the specified folder.
+ If status is true, it will enable, else it will disable.
+ Note that the user _must_ have the change_permissions permission on the
+ folder to allow changes on it.
+ If you want to use this code from a product, please use _acquireLocalRoles()
+ instead: this private method won't check security on the destination folder.
+ It's usually a bad idea to use _acquireLocalRoles() directly in your product,
+ but, well, after all, you do what you want ! :^)
+ """
+ # Perform security check on destination folder
+ if not getSecurityManager().checkPermission(Permissions.change_permissions, folder):
+ raise Unauthorized(name = "acquireLocalRoles")
+
+ return self._acquireLocalRoles(folder, status)
+ acquireLocalRoles = postonly(acquireLocalRoles)
+
+ def _acquireLocalRoles(self, folder, status):
+ """Same as _acquireLocalRoles() but won't perform security check on the folder.
+ """
+ # Set the variable (or unset it if it's defined)
+ if not status:
+ folder.__ac_local_roles_block__ = 1
+ else:
+ if getattr(folder, '__ac_local_roles_block__', None):
+ folder.__ac_local_roles_block__ = None
+
+
+ security.declarePublic("isLocalRoleAcquired")
+ def isLocalRoleAcquired(self, folder):
+ """Return true if the specified folder allows local role acquisition.
+ """
+ if getattr(folder, '__ac_local_roles_block__', None):
+ return 0
+ return 1
+
+
+ # #
+ # Security audit and info methods #
+ # #
+
+
+ # This method normally has NOT to be public ! It is because of a CMF inconsistancy.
+ # folder_localrole_form is accessible to users who have the manage_properties permissions
+ # (according to portal_types/Folder/Actions information). This is silly !
+ # folder_localrole_form should be, in CMF, accessible only to those who have the
+ # manage_users permissions instead of manage_properties permissions.
+ # This is yet another one CMF bug we have to care about.
+ # To deal with that in Plone2.1, we check for a particular permission on the destination
+ # object _inside_ the method.
+ security.declarePublic("getLocalRolesForDisplay")
+ def getLocalRolesForDisplay(self, object):
+ """This is used for plone's local roles display
+ This method returns a tuple (massagedUsername, roles, userType, actualUserName).
+ This method is protected by the 'Manage properties' permission. We may
+ change that if it's too permissive..."""
+ # Perform security check on destination object
+ if not getSecurityManager().checkPermission(Permissions.manage_properties, object):
+ raise Unauthorized(name = "getLocalRolesForDisplay")
+
+ return self._getLocalRolesForDisplay(object)
+
+ def _getLocalRolesForDisplay(self, object):
+ """This is used for plone's local roles display
+ This method returns a tuple (massagedUsername, roles, userType, actualUserName)"""
+ result = []
+ local_roles = object.get_local_roles()
+ prefix = self.getGroupPrefix()
+ for one_user in local_roles:
+ massagedUsername = username = one_user[0]
+ roles = one_user[1]
+ userType = 'user'
+ if prefix:
+ if self.getGroupById(username) is not None:
+ massagedUsername = username[len(prefix):]
+ userType = 'group'
+ else:
+ userType = 'unknown'
+ result.append((massagedUsername, roles, userType, username))
+ return tuple(result)
+
+
+ security.declarePublic("getAllLocalRoles")
+ def getAllLocalRoles(self, object):
+ """getAllLocalRoles(self, object): return a dictionnary {useratom_id: roles} of local
+ roles defined AND herited at a certain point. This will handle lr-blocking
+ as well.
+ """
+ # Perform security check on destination object
+ if not getSecurityManager().checkPermission(Permissions.change_permissions, object):
+ raise Unauthorized(name = "getAllLocalRoles")
+
+ return self._getAllLocalRoles(object)
+
+
+ def _getAllLocalRoles(self, object):
+ """getAllLocalRoles(self, object): return a dictionnary {useratom_id: roles} of local
+ roles defined AND herited at a certain point. This will handle lr-blocking
+ as well.
+ """
+ # Modified from AccessControl.User.getRolesInContext().
+ merged = {}
+ object = getattr(object, 'aq_inner', object)
+ while 1:
+ if hasattr(object, '__ac_local_roles__'):
+ dict = object.__ac_local_roles__ or {}
+ if callable(dict): dict = dict()
+ for k, v in dict.items():
+ if not merged.has_key(k):
+ merged[k] = {}
+ for role in v:
+ merged[k][role] = 1
+ if not self.isLocalRoleAcquired(object):
+ break
+ if hasattr(object, 'aq_parent'):
+ object=object.aq_parent
+ object=getattr(object, 'aq_inner', object)
+ continue
+ if hasattr(object, 'im_self'):
+ object=object.im_self
+ object=getattr(object, 'aq_inner', object)
+ continue
+ break
+ for key, value in merged.items():
+ merged[key] = value.keys()
+ return merged
+
+
+
+ # Plone-specific security matrix computing method.
+ security.declarePublic("getPloneSecurityMatrix")
+ def getPloneSecurityMatrix(self, object):
+ """getPloneSecurityMatrix(self, object): return a list of dicts of the current object
+ and all its parents. The list is sorted with portal object first.
+ Each dict has the following structure:
+ {
+ depth: (0 for portal root, 1 for 1st-level folders and so on),
+ id:
+ title:
+ icon:
+ absolute_url:
+ security_permission: true if current user can change security on this object
+ state: (workflow state)
+ acquired_local_roles: 0 if local role blocking is enabled for this folder
+ roles: {
+ 'role1': {
+ 'all_local_roles': [r1, r2, r3, ] (all defined local roles, including parent ones)
+ 'defined_local_roles': [r3, ] (local-defined only local roles)
+ 'permissions': ['Access contents information', 'Modify portal content', ] (only a subset)
+ 'same_permissions': true if same permissions as the parent
+ 'same_all_local_roles': true if all_local_roles is the same as the parent
+ 'same_defined_local_roles': true if defined_local_roles is the same as the parent
+ },
+ 'role2': {...},
+ },
+ }
+ """
+ # Perform security check on destination object
+ if not getSecurityManager().checkPermission(Permissions.access_contents_information, object):
+ raise Unauthorized(name = "getPloneSecurityMatrix")
+
+ # Basic inits
+ mt = self.portal_membership
+
+ # Fetch all possible roles in the portal
+ all_roles = ['Anonymous'] + mt.getPortalRoles()
+
+ # Fetch parent folders list until the portal
+ all_objects = []
+ cur_object = object
+ while 1:
+ if not getSecurityManager().checkPermission(Permissions.access_contents_information, cur_object):
+ raise Unauthorized(name = "getPloneSecurityMatrix")
+ all_objects.append(cur_object)
+ if cur_object.meta_type == "Plone Site":
+ break
+ cur_object = object.aq_parent
+ all_objects.reverse()
+
+ # Scan those folders to get all the required information about them
+ ret = []
+ previous = None
+ count = 0
+ for obj in all_objects:
+ # Basic information
+ current = {
+ "depth": count,
+ "id": obj.getId(),
+ "title": obj.Title(),
+ "icon": obj.getIcon(),
+ "absolute_url": obj.absolute_url(),
+ "security_permission": getSecurityManager().checkPermission(Permissions.change_permissions, obj),
+ "acquired_local_roles": self.isLocalRoleAcquired(obj),
+ "roles": {},
+ "state": "XXX TODO XXX", # XXX TODO
+ }
+ count += 1
+
+ # Workflow state
+ # XXX TODO
+
+ # Roles
+ all_local_roles = {}
+ local_roles = self._getAllLocalRoles(obj)
+ for user, roles in self._getAllLocalRoles(obj).items():
+ for role in roles:
+ if not all_local_roles.has_key(role):
+ all_local_roles[role] = {}
+ all_local_roles[role][user] = 1
+ defined_local_roles = {}
+ if hasattr(obj.aq_base, 'get_local_roles'):
+ for user, roles in obj.get_local_roles():
+ for role in roles:
+ if not defined_local_roles.has_key(role):
+ defined_local_roles[role] = {}
+ defined_local_roles[role][user] = 1
+
+ for role in all_roles:
+ all = all_local_roles.get(role, {}).keys()
+ defined = defined_local_roles.get(role, {}).keys()
+ all.sort()
+ defined.sort()
+ same_all_local_roles = 0
+ same_defined_local_roles = 0
+ if previous:
+ if previous['roles'][role]['all_local_roles'] == all:
+ same_all_local_roles = 1
+ if previous['roles'][role]['defined_local_roles'] == defined:
+ same_defined_local_roles = 1
+
+ current['roles'][role] = {
+ "all_local_roles": all,
+ "defined_local_roles": defined,
+ "same_all_local_roles": same_all_local_roles,
+ "same_defined_local_roles": same_defined_local_roles,
+ "permissions": [], # XXX TODO
+ }
+
+ ret.append(current)
+ previous = current
+
+ return ret
+
+
+ security.declareProtected(Permissions.manage_users, "computeSecuritySettings")
+ def computeSecuritySettings(self, folders, actors, permissions, cache = {}):
+ """
+ computeSecuritySettings(self, folders, actors, permissions, cache = {}) => return a structure that is suitable for security audit Page Template.
+
+ - folders is the structure returned by getSiteTree()
+ - actors is the structure returned by listUsersAndRoles()
+ - permissions is ((id: permission), (id: permission), ...)
+ - cache is passed along requests to make computing faster
+ """
+ # Scan folders and actors to get the relevant information
+ usr_cache = {}
+ for id, depth, path in folders:
+ folder = self.unrestrictedTraverse(path)
+ for kind, actor, display, handle, html in actors:
+ if kind in ("user", "group"):
+ # Init structure
+ if not cache.has_key(path):
+ cache[path] = {(kind, actor): {}}
+ elif not cache[path].has_key((kind, actor)):
+ cache[path][(kind, actor)] = {}
+ else:
+ cache[path][(kind, actor)] = {}
+
+ # Split kind into groups and get individual role information
+ perm_keys = []
+ usr = usr_cache.get(actor)
+ if not usr:
+ usr = self.getUser(actor)
+ usr_cache[actor] = usr
+ roles = usr.getRolesInContext(folder,)
+ for role in roles:
+ for perm_key in self.computeSetting(path, folder, role, permissions, cache).keys():
+ cache[path][(kind, actor)][perm_key] = 1
+
+ else:
+ # Get role information
+ self.computeSetting(path, folder, actor, permissions, cache)
+
+ # Return the computed cache
+ return cache
+
+
+ security.declareProtected(Permissions.manage_users, "computeSetting")
+ def computeSetting(self, path, folder, actor, permissions, cache):
+ """
+ computeSetting(......) => used by computeSecuritySettings to populate the cache for ROLES
+ """
+ # Avoid doing things twice
+ kind = "role"
+ if cache.get(path, {}).get((kind, actor), None) is not None:
+ return cache[path][(kind, actor)]
+
+ # Initilize cache structure
+ if not cache.has_key(path):
+ cache[path] = {(kind, actor): {}}
+ elif not cache[path].has_key((kind, actor)):
+ cache[path][(kind, actor)] = {}
+
+ # Analyze permission settings
+ ps = folder.permission_settings()
+ for perm_key, permission in permissions:
+ # Check acquisition of permission setting.
+ can = 0
+ acquired = 0
+ for p in ps:
+ if p['name'] == permission:
+ acquired = not not p['acquire']
+
+ # If acquired, call the parent recursively
+ if acquired:
+ parent = folder.aq_parent.getPhysicalPath()
+ perms = self.computeSetting(parent, self.unrestrictedTraverse(parent), actor, permissions, cache)
+ can = perms.get(perm_key, None)
+
+ # Else, check permission here
+ else:
+ for p in folder.rolesOfPermission(permission):
+ if p['name'] == "Anonymous":
+ # If anonymous is allowed, then everyone is allowed
+ if p['selected']:
+ can = 1
+ break
+ if p['name'] == actor:
+ if p['selected']:
+ can = 1
+ break
+
+ # Extend the data structure according to 'can' setting
+ if can:
+ cache[path][(kind, actor)][perm_key] = 1
+
+ return cache[path][(kind, actor)]
+
+
+ security.declarePrivate('_getNextHandle')
+ def _getNextHandle(self, index):
+ """
+ _getNextHandle(self, index) => utility function to
+ get an unique handle for each legend item.
+ """
+ return "%02d" % index
+
+
+ security.declareProtected(Permissions.manage_users, "listUsersAndRoles")
+ def listUsersAndRoles(self,):
+ """
+ listUsersAndRoles(self,) => list of tuples
+
+ This method is used by the Security Audit page.
+ XXX HAS TO BE OPTIMIZED
+ """
+ request = self.REQUEST
+ display_roles = request.get('display_roles', 0)
+ display_groups = request.get('display_groups', 0)
+ display_users = request.get('display_users', 0)
+
+ role_index = 0
+ user_index = 0
+ group_index = 0
+ ret = []
+
+ # Collect roles
+ if display_roles:
+ for r in self.aq_parent.valid_roles():
+ handle = "R%02d" % role_index
+ role_index += 1
+ ret.append(('role', r, r, handle, r))
+
+ # Collect users
+ if display_users:
+ for u in map(lambda x: x.getId(), self.getPureUsers()):
+ obj = self.getUser(u)
+ html = obj.asHTML()
+ handle = "U%02d" % user_index
+ user_index += 1
+ ret.append(('user', u, u, handle, html))
+
+ if display_groups:
+ for u in self.getGroupNames():
+ obj = self.getUser(u)
+ handle = "G%02d" % group_index
+ html = obj.asHTML()
+ group_index += 1
+ ret.append(('group', u, obj.getUserNameWithoutGroupPrefix(), handle, html))
+
+ # Return list
+ return ret
+
+ security.declareProtected(Permissions.manage_users, "getSiteTree")
+ def getSiteTree(self, obj=None, depth=0):
+ """
+ getSiteTree(self, obj=None, depth=0) => special structure
+
+ This is used by the security audit page
+ """
+ ret = []
+ if not obj:
+ if depth==0:
+ obj = self.aq_parent
+ else:
+ return ret
+
+ ret.append([obj.getId(), depth, string.join(obj.getPhysicalPath(), '/')])
+ for sub in obj.objectValues():
+ try:
+ # Ignore user folders
+ if sub.getId() in ('acl_users', ):
+ continue
+
+ # Ignore portal_* stuff
+ if sub.getId()[:len('portal_')] == 'portal_':
+ continue
+
+ if sub.isPrincipiaFolderish:
+ ret.extend(self.getSiteTree(sub, depth + 1))
+
+ except:
+ # We ignore exceptions
+ pass
+
+ return ret
+
+ security.declareProtected(Permissions.manage_users, "listAuditPermissions")
+ def listAuditPermissions(self,):
+ """
+ listAuditPermissions(self,) => return a list of eligible permissions
+ """
+ ps = self.permission_settings()
+ return map(lambda p: p['name'], ps)
+
+ security.declareProtected(Permissions.manage_users, "getDefaultPermissions")
+ def getDefaultPermissions(self,):
+ """
+ getDefaultPermissions(self,) => return default R & W permissions for security audit.
+ """
+ # If there's a Plone site in the above folder, use plonish permissions
+ hasPlone = 0
+ p = self.aq_parent
+ if p.meta_type == "CMF Site":
+ hasPlone = 1
+ else:
+ for obj in p.objectValues():
+ if obj.meta_type == "CMF Site":
+ hasPlone = 1
+ break
+
+ if hasPlone:
+ return {'R': 'View',
+ 'W': 'Modify portal content',
+ }
+ else:
+ return {'R': 'View',
+ 'W': 'Change Images and Files',
+ }
+
+
+ # #
+ # Users/Groups tree view #
+ # (ZMI only) #
+ # #
+
+
+ security.declarePrivate('getTreeInfo')
+ def getTreeInfo(self, usr, dict = {}):
+ "utility method"
+ # Prevend infinite recursions
+ name = usr.getUserName()
+ if dict.has_key(name):
+ return
+ dict[name] = {}
+
+ # Properties
+ noprefix = usr.getUserNameWithoutGroupPrefix()
+ is_group = usr.isGroup()
+ if usr.isGroup():
+ icon = string.join(self.getPhysicalPath(), '/') + '/img_group'
+## icon = self.absolute_url() + '/img_group'
+ else:
+ icon = ' img_user'
+## icon = self.absolute_url() + '/img_user'
+
+ # Subobjects
+ belongs_to = []
+ for grp in usr.getGroups(no_recurse = 1):
+ belongs_to.append(grp)
+ self.getTreeInfo(self.getGroup(grp))
+
+ # Append (and return) structure
+ dict[name] = {
+ "name": noprefix,
+ "is_group": is_group,
+ "icon": icon,
+ "belongs_to": belongs_to,
+ }
+ return dict
+
+
+ security.declarePrivate("tpValues")
+ def tpValues(self):
+ # Avoid returning HUUUUUUGE lists
+ # Use the cache at first
+ if self._v_no_tree and self._v_cache_no_tree > time.time():
+ return [] # Do not use the tree
+
+ # XXX - I DISABLE THE TREE BY NOW (Pb. with icon URL)
+ return []
+
+ # Then, use a simple computation to determine opportunity to use the tree or not
+ ngroups = len(self.getGroupNames())
+ if ngroups > MAX_TREE_USERS_AND_GROUPS:
+ self._v_no_tree = 1
+ self._v_cache_no_tree = time.time() + TREE_CACHE_TIME
+ return []
+ nusers = len(self.getUsers())
+ if ngroups + nusers > MAX_TREE_USERS_AND_GROUPS:
+ meth_list = self.getGroups
+ else:
+ meth_list = self.getUsers
+ self._v_no_tree = 0
+
+ # Get top-level user and groups list
+ tree_dict = {}
+ top_level_names = []
+ top_level = []
+ for usr in meth_list():
+ self.getTreeInfo(usr, tree_dict)
+ if not usr.getGroups(no_recurse = 1):
+ top_level_names.append(usr.getUserName())
+ for id in top_level_names:
+ top_level.append(treeWrapper(id, tree_dict))
+
+ # Return this top-level list
+ top_level.sort(lambda x, y: cmp(x.sortId(), y.sortId()))
+ return top_level
+
+
+ def tpId(self,):
+ return self.getId()
+
+
+ # #
+ # Direct traversal to user or group info #
+ # #
+
+ def manage_workspace(self, REQUEST):
+ """
+ manage_workspace(self, REQUEST) => Overrided to allow direct user or group traversal
+ via the left tree view.
+ """
+ path = string.split(REQUEST.PATH_INFO, '/')[:-1]
+ userid = path[-1]
+
+ # Use individual usr/grp management screen (only if name is passed along the mgt URL)
+ if userid != "acl_users":
+ usr = self.getUserById(userid)
+ if usr:
+ REQUEST.set('username', userid)
+ REQUEST.set('MANAGE_TABS_NO_BANNER', '1') # Prevent use of the manage banner
+ return self.restrictedTraverse('manage_user')()
+
+ # Default management screen
+ return self.restrictedTraverse('manage_overview')()
+
+
+ # Tree caching information
+ _v_no_tree = 0
+ _v_cache_no_tree = 0
+ _v_cache_tree = (0, [])
+
+
+ def __bobo_traverse__(self, request, name):
+ """
+ Looks for the name of a user or a group.
+ This applies only if users list is not huge.
+ """
+ # Check if it's an attribute
+ if hasattr(self.aq_base, name, ):
+ return getattr(self, name)
+
+ # It's not an attribute, maybe it's a user/group
+ # (this feature is used for the tree)
+ if name.startswith('_'):
+ pass # Do not fetch users
+ elif name.startswith('manage_'):
+ pass # Do not fetch users
+ elif name in INVALID_USER_NAMES:
+ pass # Do not fetch users
+ else:
+ # Only try to get users is fetch_user is true.
+ # This is only for performance reasons.
+ # The following code block represent what we want to minimize
+ if self._v_cache_tree[0] < time.time():
+ un = map(lambda x: x.getId(), self.getUsers()) # This is the cost we want to avoid
+ self._v_cache_tree = (time.time() + TREE_CACHE_TIME, un, )
+ else:
+ un = self._v_cache_tree[1]
+
+ # Get the user if we can
+ if name in un:
+ self._v_no_tree = 0
+ return self
+
+ # Force getting the user if we must
+ if request.get("FORCE_USER"):
+ self._v_no_tree = 0
+ return self
+
+ # This will raise if it's not possible to acquire 'name'
+ return getattr(self, name, )
+
+
+
+ # #
+ # USERS / GROUPS BATCHING (ZMI SCREENS) #
+ # #
+
+ _v_batch_users = []
+
+ security.declareProtected(Permissions.view_management_screens, "listUsersBatches")
+ def listUsersBatches(self,):
+ """
+ listUsersBatches(self,) => return a list of (start, end) tuples.
+ Return None if batching is not necessary
+ """
+ # Time-consuming stuff !
+ un = map(lambda x: x.getId(), self.getPureUsers())
+ if len(un) <= MAX_USERS_PER_PAGE:
+ return None
+ un.sort()
+
+ # Split this list into small groups if necessary
+ ret = []
+ idx = 0
+ l_un = len(un)
+ nbatches = int(math.ceil(l_un / float(MAX_USERS_PER_PAGE)))
+ for idx in range(0, nbatches):
+ first = idx * MAX_USERS_PER_PAGE
+ last = first + MAX_USERS_PER_PAGE - 1
+ if last >= l_un:
+ last = l_un - 1
+ # Append a tuple (not dict) to avoid too much memory consumption
+ ret.append((first, last, un[first], un[last]))
+
+ # Cache & return it
+ self._v_batch_users = un
+ return ret
+
+ security.declareProtected(Permissions.view_management_screens, "listUsersBatchTable")
+ def listUsersBatchTable(self,):
+ """
+ listUsersBatchTable(self,) => Same a mgt screens but divided into sublists to
+ present them into 5 columns.
+ XXX have to merge this w/getUsersBatch to make it in one single pass
+ """
+ # Iterate
+ ret = []
+ idx = 0
+ current = []
+ for rec in (self.listUsersBatches() or []):
+ if not idx % 5:
+ if current:
+ ret.append(current)
+ current = []
+ current.append(rec)
+ idx += 1
+
+ if current:
+ ret.append(current)
+
+ return ret
+
+ security.declareProtected(Permissions.view_management_screens, "getUsersBatch")
+ def getUsersBatch(self, start):
+ """
+ getUsersBatch(self, start) => user list
+ """
+ # Rebuild the list if necessary
+ if not self._v_batch_users:
+ un = map(lambda x: x.getId(), self.getPureUsers())
+ self._v_batch_users = un
+
+ # Return the batch
+ end = start + MAX_USERS_PER_PAGE
+ ids = self._v_batch_users[start:end]
+ ret = []
+ for id in ids:
+ usr = self.getUser(id)
+ if usr: # Prevent adding invalid users
+ ret.append(usr)
+ return ret
+
+
+ # #
+ # Multiple sources management #
+ # #
+
+ # Arrows
+ img_up_arrow = ImageFile.ImageFile('www/up_arrow.gif', globals())
+ img_down_arrow = ImageFile.ImageFile('www/down_arrow.gif', globals())
+ img_up_arrow_grey = ImageFile.ImageFile('www/up_arrow_grey.gif', globals())
+ img_down_arrow_grey = ImageFile.ImageFile('www/down_arrow_grey.gif', globals())
+
+ security.declareProtected(Permissions.manage_users, "toggleSource")
+ def toggleSource(self, src_id, REQUEST = {}):
+ """
+ toggleSource(self, src_id, REQUEST = {}) => toggle enabled/disabled source
+ """
+ # Find the source
+ ids = self.objectIds('GRUFUsers')
+ if not src_id in ids:
+ raise ValueError, "Invalid source: '%s' (%s)" % (src_id, ids)
+ src = getattr(self, src_id)
+ if src.enabled:
+ src.disableSource()
+ else:
+ src.enableSource()
+
+ # Redirect where we want to
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_GRUFSources')
+
+
+ security.declareProtected(Permissions.manage_users, "listUserSources")
+ def listUserSources(self, ):
+ """
+ listUserSources(self, ) => Return a list of userfolder objects
+ Only return VALID (ie containing an acl_users) user sources if all is None
+ XXX HAS TO BE OPTIMIZED VERY MUCH!
+ We add a check in debug mode to ensure that invalid sources won't be added
+ to the list.
+ This method return only _enabled_ user sources.
+ """
+ ret = []
+ dret = {}
+ if DEBUG_MODE:
+ for src in self.objectValues(['GRUFUsers']):
+ if not src.enabled:
+ continue
+ if 'acl_users' in src.objectIds():
+ if getattr(aq_base(src.acl_users), 'authenticate', None): # Additional check in debug mode
+ dret[src.id] = src.acl_users # we cannot use restrictedTraverse here because
+ # of infinite recursion issues.
+ else:
+ for src in self.objectValues(['GRUFUsers']):
+ if not src.enabled:
+ continue
+ if not 'acl_users' in src.objectIds():
+ continue
+ dret[src.id] = src.acl_users
+ ret = dret.items()
+ ret.sort()
+ return [ src[1] for src in ret ]
+
+ security.declareProtected(Permissions.manage_users, "listUserSourceFolders")
+ def listUserSourceFolders(self, ):
+ """
+ listUserSources(self, ) => Return a list of GRUFUsers objects
+ """
+ ret = []
+ for src in self.objectValues(['GRUFUsers']):
+ ret.append(src)
+ ret.sort(lambda x,y: cmp(x.id, y.id))
+ return ret
+
+ security.declarePrivate("getUserSource")
+ def getUserSource(self, id):
+ """
+ getUserSource(self, id) => GRUFUsers.acl_users object.
+ Raises if no acl_users available
+ """
+ return getattr(self, id).acl_users
+
+ security.declarePrivate("getUserSourceFolder")
+ def getUserSourceFolder(self, id):
+ """
+ getUserSourceFolder(self, id) => GRUFUsers object
+ """
+ return getattr(self, id)
+
+ security.declareProtected(Permissions.manage_users, "addUserSource")
+ def addUserSource(self, factory_uri, REQUEST = {}, *args, **kw):
+ """
+ addUserSource(self, factory_uri, REQUEST = {}, *args, **kw) => redirect
+ Adds the specified user folder
+ """
+ # Get the initial Users id
+ ids = self.objectIds('GRUFUsers')
+ if ids:
+ ids.sort()
+ if ids == ['Users',]:
+ last = 0
+ else:
+ last = int(ids[-1][-2:])
+ next_id = "Users%02d" % (last + 1, )
+ else:
+ next_id = "Users"
+
+ # Add the GRUFFolder object
+ uf = GRUFFolder.GRUFUsers(id = next_id)
+ self._setObject(next_id, uf)
+
+## # If we use ldap, tag it
+## if string.find(factory_uri.lower(), "ldap") > -1:
+## self._haveLDAPUF += 1
+
+ # Add its underlying UserFolder
+ # If we're called TTW, uses a redirect else tries to call the UF factory directly
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect("%s/%s/%s" % (self.absolute_url(), next_id, factory_uri))
+ return getattr(self, next_id).unrestrictedTraverse(factory_uri)(*args, **kw)
+ addUserSource = postonly(addUserSource)
+
+ security.declareProtected(Permissions.manage_users, "deleteUserSource")
+ def deleteUserSource(self, id = None, REQUEST = {}):
+ """
+ deleteUserSource(self, id = None, REQUEST = {}) => Delete the specified user source
+ """
+ # Check the source id
+ if type(id) != type('s'):
+ raise ValueError, "You must choose a valid source to delete and confirm it."
+
+ # Delete it
+ self.manage_delObjects([id,])
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_GRUFSources')
+ deleteUserSource = postonly(deleteUserSource)
+
+ security.declareProtected(Permissions.manage_users, "getDefaultUserSource")
+ def getDefaultUserSource(self,):
+ """
+ getDefaultUserSource(self,) => acl_users object
+ Return default user source for user writing.
+ XXX By now, the FIRST source is the default one. This may change in the future.
+ """
+ lst = self.listUserSources()
+ if not lst:
+ raise RuntimeError, "No valid User Source to add users in."
+ return lst[0]
+
+
+ security.declareProtected(Permissions.manage_users, "listAvailableUserSources")
+ def listAvailableUserSources(self, filter_permissions = 1, filter_classes = 1):
+ """
+ listAvailableUserSources(self, filter_permissions = 1, filter_classes = 1) => tuples (name, factory_uri)
+ List UserFolder replacement candidates.
+
+ - if filter_classes is true, return only ones which have a base UserFolder class
+ - if filter_permissions, return only types the user has rights to add
+ """
+ ret = []
+
+ # Fetch candidate types
+ user = getSecurityManager().getUser()
+ meta_types = []
+ if callable(self.all_meta_types):
+ all=self.all_meta_types()
+ else:
+ all=self.all_meta_types
+ for meta_type in all:
+ if filter_permissions and meta_type.has_key('permission'):
+ if user.has_permission(meta_type['permission'],self):
+ meta_types.append(meta_type)
+ else:
+ meta_types.append(meta_type)
+
+ # Keep only, if needed, BasicUserFolder-derived classes
+ for t in meta_types:
+ if t['name'] == self.meta_type:
+ continue # Do not keep GRUF ! ;-)
+
+ if filter_classes:
+ try:
+ if t.get('instance', None) and t['instance'].isAUserFolder:
+ ret.append((t['name'], t['action']))
+ continue
+ if t.get('instance', None) and class_utility.isBaseClass(AccessControl.User.BasicUserFolder, t['instance']):
+ ret.append((t['name'], t['action']))
+ continue
+ except AttributeError:
+ pass # We ignore 'invalid' instances (ie. that wouldn't define a __base__ attribute)
+ else:
+ ret.append((t['name'], t['action']))
+
+ return tuple(ret)
+
+ security.declareProtected(Permissions.manage_users, "moveUserSourceUp")
+ def moveUserSourceUp(self, id, REQUEST = {}):
+ """
+ moveUserSourceUp(self, id, REQUEST = {}) => used in management screens
+ try to get ids as consistant as possible
+ """
+ # List and sort sources and preliminary checks
+ ids = self.objectIds('GRUFUsers')
+ ids.sort()
+ if not ids or not id in ids:
+ raise ValueError, "Invalid User Source: '%s'" % (id,)
+
+ # Find indexes to swap
+ src_index = ids.index(id)
+ if src_index == 0:
+ raise ValueError, "Cannot move '%s' User Source up." % (id, )
+ dest_index = src_index - 1
+
+ # Find numbers to swap, fix them if they have more than 1 as offset
+ if ids[dest_index] == 'Users':
+ dest_num = 0
+ else:
+ dest_num = int(ids[dest_index][-2:])
+ src_num = dest_num + 1
+
+ # Get ids
+ src_id = id
+ if dest_num == 0:
+ dest_id = "Users"
+ else:
+ dest_id = "Users%02d" % (dest_num,)
+ tmp_id = "%s_" % (dest_id, )
+
+ # Perform the swap
+ self._renameUserSource(src_id, tmp_id)
+ self._renameUserSource(dest_id, src_id)
+ self._renameUserSource(tmp_id, dest_id)
+
+ # Return back to the forms
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_GRUFSources')
+ moveUserSourceUp = postonly(moveUserSourceUp)
+
+ security.declareProtected(Permissions.manage_users, "moveUserSourceDown")
+ def moveUserSourceDown(self, id, REQUEST = {}):
+ """
+ moveUserSourceDown(self, id, REQUEST = {}) => used in management screens
+ try to get ids as consistant as possible
+ """
+ # List and sort sources and preliminary checks
+ ids = self.objectIds('GRUFUsers')
+ ids.sort()
+ if not ids or not id in ids:
+ raise ValueError, "Invalid User Source: '%s'" % (id,)
+
+ # Find indexes to swap
+ src_index = ids.index(id)
+ if src_index == len(ids) - 1:
+ raise ValueError, "Cannot move '%s' User Source up." % (id, )
+ dest_index = src_index + 1
+
+ # Find numbers to swap, fix them if they have more than 1 as offset
+ if id == 'Users':
+ dest_num = 1
+ else:
+ dest_num = int(ids[dest_index][-2:])
+ src_num = dest_num - 1
+
+ # Get ids
+ src_id = id
+ if dest_num == 0:
+ dest_id = "Users"
+ else:
+ dest_id = "Users%02d" % (dest_num,)
+ tmp_id = "%s_" % (dest_id, )
+
+ # Perform the swap
+ self._renameUserSource(src_id, tmp_id)
+ self._renameUserSource(dest_id, src_id)
+ self._renameUserSource(tmp_id, dest_id)
+
+ # Return back to the forms
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_GRUFSources')
+ moveUserSourceDown = postonly(moveUserSourceDown)
+
+
+ security.declarePrivate('_renameUserSource')
+ def _renameUserSource(self, id, new_id, ):
+ """
+ Rename a particular sub-object.
+ Taken fro CopySupport.manage_renameObject() code, modified to disable verifications.
+ """
+ try: self._checkId(new_id)
+ except: raise CopyError, MessageDialog(
+ title='Invalid Id',
+ message=sys.exc_info()[1],
+ action ='manage_main')
+ ob=self._getOb(id)
+## if not ob.cb_isMoveable():
+## raise "Copy Error", eNotSupported % id
+## self._verifyObjectPaste(ob) # This is what we disable
+ try: ob._notifyOfCopyTo(self, op=1)
+ except: raise CopyError, MessageDialog(
+ title='Rename Error',
+ message=sys.exc_info()[1],
+ action ='manage_main')
+ self._delObject(id)
+ ob = aq_base(ob)
+ ob._setId(new_id)
+
+ # Note - because a rename always keeps the same context, we
+ # can just leave the ownership info unchanged.
+ self._setObject(new_id, ob, set_owner=0)
+
+
+ security.declareProtected(Permissions.manage_users, "replaceUserSource")
+ def replaceUserSource(self, id = None, new_factory = None, REQUEST = {}, *args, **kw):
+ """
+ replaceUserSource(self, id = None, new_factory = None, REQUEST = {}, *args, **kw) => perform user source replacement
+
+ If new_factory is None, find it inside REQUEST (useful for ZMI screens)
+ """
+ # Check the source id
+ if type(id) != type('s'):
+ raise ValueError, "You must choose a valid source to replace and confirm it."
+
+ # Retreive factory if not explicitly passed
+ if not new_factory:
+ for record in REQUEST.get("source_rec", []):
+ if record['id'] == id:
+ new_factory = record['new_factory']
+ break
+ if not new_factory:
+ raise ValueError, "You must select a new User Folder type."
+
+ # Delete the former one
+ us = getattr(self, id)
+ if "acl_users" in us.objectIds():
+ us.manage_delObjects(['acl_users'])
+
+ ## If we use ldap, tag it
+ #if string.find(new_factory.lower(), "ldap") > -1:
+ # self._haveLDAPUF += 1
+
+ # Re-create the underlying UserFolder
+ # If we're called TTW, uses a redirect else tries to call the UF factory directly
+ if REQUEST.has_key('RESPONSE'):
+ return REQUEST.RESPONSE.redirect("%s/%s/%s" % (self.absolute_url(), id, new_factory))
+ return us.unrestrictedTraverse(new_factory)(*args, **kw) # XXX minor security pb ?
+ replaceUserSource = postonly(replaceUserSource)
+
+
+ security.declareProtected(Permissions.manage_users, "hasLDAPUserFolderSource")
+ def hasLDAPUserFolderSource(self, ):
+ """
+ hasLDAPUserFolderSource(self,) => boolean
+ Return true if a LUF source is instanciated.
+ """
+ for src in self.listUserSources():
+ if src.meta_type == "LDAPUserFolder":
+ return 1
+ return None
+
+
+ security.declareProtected(Permissions.manage_users, "updateLDAPUserFolderMapping")
+ def updateLDAPUserFolderMapping(self, REQUEST = None):
+ """
+ updateLDAPUserFolderMapping(self, REQUEST = None) => None
+
+ Update the first LUF source in the process so that LDAP-group-to-Zope-role mapping
+ is done.
+ This is done by calling the appropriate method in LUF and affecting all 'group_' roles
+ to the matching LDAP groups.
+ """
+ # Fetch all groups
+ groups = self.getGroupIds()
+
+ # Scan sources
+ for src in self.listUserSources():
+ if not src.meta_type == "LDAPUserFolder":
+ continue
+
+ # Delete all former group mappings
+ deletes = []
+ for (grp, role) in src.getGroupMappings():
+ if role.startswith('group_'):
+ deletes.append(grp)
+ src.manage_deleteGroupMappings(deletes)
+
+ # Append all group mappings if it can be done
+ ldap_groups = src.getGroups(attr = "cn")
+ for grp in groups:
+ if src._local_groups:
+ grp_name = grp
+ else:
+ grp_name = grp[len('group_'):]
+ Log(LOG_DEBUG, "cheching", grp_name, "in", ldap_groups, )
+ if not grp_name in ldap_groups:
+ continue
+ Log(LOG_DEBUG, "Map", grp, "to", grp_name)
+ src.manage_addGroupMapping(
+ grp_name,
+ grp,
+ )
+
+ # Return
+ if REQUEST:
+ return REQUEST.RESPONSE.redirect(
+ self.absolute_url() + "/manage_wizard",
+ )
+ updateLDAPUserFolderMapping = postonly(updateLDAPUserFolderMapping)
+
+
+ # #
+ # The Wizard Section #
+ # #
+
+ def listLDAPUserFolderMapping(self,):
+ """
+ listLDAPUserFolderMapping(self,) => utility method
+ """
+ ret = []
+ gruf_done = []
+ ldap_done = []
+
+ # Scan sources
+ for src in self.listUserSources():
+ if not src.meta_type == "LDAPUserFolder":
+ continue
+
+ # Get all GRUF & LDAP groups
+ if src._local_groups:
+ gruf_ids = self.getGroupIds()
+ else:
+ gruf_ids = self.getGroupIds()
+ ldap_mapping = src.getGroupMappings()
+ ldap_groups = src.getGroups(attr = "cn")
+ for grp,role in ldap_mapping:
+ if role in gruf_ids:
+ ret.append((role, grp))
+ gruf_done.append(role)
+ ldap_done.append(grp)
+ if not src._local_groups:
+ ldap_done.append(role)
+ for grp in ldap_groups:
+ if not grp in ldap_done:
+ ret.append((None, grp))
+ for grp in gruf_ids:
+ if not grp in gruf_done:
+ ret.append((grp, None))
+ Log(LOG_DEBUG, "return", ret)
+ return ret
+
+
+ security.declareProtected(Permissions.manage_users, "getInvalidMappings")
+ def getInvalidMappings(self,):
+ """
+ return true if LUF mapping looks good
+ """
+ wrong = []
+ grufs = []
+ for gruf, ldap in self.listLDAPUserFolderMapping():
+ if gruf and ldap:
+ continue
+ if not gruf:
+ continue
+ if gruf.startswith('group_'):
+ gruf = gruf[len('group_'):]
+ grufs.append(gruf)
+ for gruf, ldap in self.listLDAPUserFolderMapping():
+ if gruf and ldap:
+ continue
+ if not ldap:
+ continue
+ if ldap.startswith('group_'):
+ ldap = ldap[len('group_'):]
+ if ldap in grufs:
+ wrong.append(ldap)
+
+ return wrong
+
+ security.declareProtected(Permissions.manage_users, "getLUFSource")
+ def getLUFSource(self,):
+ """
+ getLUFSource(self,) => Helper to get a pointer to the LUF src.
+ Return None if not available
+ """
+ for src in self.listUserSources():
+ if src.meta_type == "LDAPUserFolder":
+ return src
+
+ security.declareProtected(Permissions.manage_users, "areLUFGroupsLocal")
+ def areLUFGroupsLocal(self,):
+ """return true if luf groups are stored locally"""
+ return hasattr(self.getLUFSource(), '_local_groups')
+
+
+ security.declareProtected(Permissions.manage_users, "haveLDAPGroupFolder")
+ def haveLDAPGroupFolder(self,):
+ """return true if LDAPGroupFolder is the groups source
+ """
+ return not not self.Groups.acl_users.meta_type == 'LDAPGroupFolder'
+
+ security.declarePrivate('searchGroups')
+ def searchGroups(self, **kw):
+ names = self.getUserNames(__include_users__ = 0, __groups_prefixed__ = 1)
+ return [{'id' : gn} for gn in names]
+
+
+
+class treeWrapper:
+ """
+ treeWrapper: Wrapper around user/group objects for the tree
+ """
+ def __init__(self, id, tree, parents = []):
+ """
+ __init__(self, id, tree, parents = []) => wraps the user object for dtml-tree
+ """
+ # Prepare self-contained information
+ self._id = id
+ self.name = tree[id]['name']
+ self.icon = tree[id]['icon']
+ self.is_group = tree[id]['is_group']
+ parents.append(id)
+ self.path = parents
+
+ # Prepare subobjects information
+ subobjects = []
+ for grp_id in tree.keys():
+ if id in tree[grp_id]['belongs_to']:
+ subobjects.append(treeWrapper(grp_id, tree, parents))
+ subobjects.sort(lambda x, y: cmp(x.sortId(), y.sortId()))
+ self.subobjects = subobjects
+
+ def id(self,):
+ return self.name
+
+ def sortId(self,):
+ if self.is_group:
+ return "__%s" % (self._id,)
+ else:
+ return self._id
+
+ def tpValues(self,):
+ """
+ Return 'subobjects'
+ """
+ return self.subobjects
+
+ def tpId(self,):
+ return self._id
+
+ def tpURL(self,):
+ return self.tpId()
+
+InitializeClass(GroupUserFolder)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+## Copyright (c) 2003 The Connexions Project, All Rights Reserved
+## initially written by J Cameron Cooper, 11 June 2003
+## concept with Brent Hendricks, George Runyan
+"""
+Basic usergroup tool.
+"""
+__version__ = "$Revision$"
+# $Source: $
+# $Id: GroupsTool.py 50142 2007-09-25 13:13:12Z wichert $
+__docformat__ = 'restructuredtext'
+
+from Products.CMFCore.utils import UniqueObject
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.utils import _checkPermission
+from OFS.SimpleItem import SimpleItem
+from Globals import InitializeClass, DTMLFile, MessageDialog
+from Acquisition import aq_base
+from AccessControl.User import nobody
+from AccessControl import ClassSecurityInfo
+from ZODB.POSException import ConflictError
+# BBB CMF < 1.5
+try:
+ from Products.CMFCore.permissions import ManagePortal
+ from Products.CMFCore.permissions import View
+ from Products.CMFCore.permissions import ViewManagementScreens
+except ImportError:
+ from Products.CMFCore.CMFCorePermissions import ManagePortal
+ from Products.CMFCore.CMFCorePermissions import View
+ from Products.CMFCore.CMFCorePermissions import ViewManagementScreens
+
+from Products.GroupUserFolder import postonly
+from GroupsToolPermissions import AddGroups
+from GroupsToolPermissions import ManageGroups
+from GroupsToolPermissions import DeleteGroups
+from GroupsToolPermissions import ViewGroups
+from GroupsToolPermissions import SetGroupOwnership
+from Products.CMFCore.ActionProviderBase import ActionProviderBase
+from interfaces.portal_groups import portal_groups as IGroupsTool
+from global_symbols import *
+
+# Optional feature-preview support
+import PloneFeaturePreview
+
+class GroupsTool (UniqueObject, SimpleItem, ActionProviderBase, ):
+ """ This tool accesses group data through a GRUF acl_users object.
+
+ It can be replaced with something that groups member data in a
+ different way.
+ """
+ # Show implementation only if IGroupsTool is defined
+ # The latter will work only with Plone 1.1 => hence, the if
+ if hasattr(ActionProviderBase, '__implements__'):
+ __implements__ = (IGroupsTool, ActionProviderBase.__implements__)
+
+ id = 'portal_groups'
+ meta_type = 'CMF Groups Tool'
+ _actions = ()
+
+ security = ClassSecurityInfo()
+
+ groupworkspaces_id = "groups"
+ groupworkspaces_title = "Groups"
+ groupWorkspacesCreationFlag = 1
+ groupWorkspaceType = "Folder"
+ groupWorkspaceContainerType = "Folder"
+
+ manage_options=(
+ ( { 'label' : 'Configure'
+ , 'action' : 'manage_config'
+ },
+ ) + ActionProviderBase.manage_options +
+ ( { 'label' : 'Overview'
+ , 'action' : 'manage_overview'
+ },
+ ) + SimpleItem.manage_options)
+
+ # #
+ # ZMI methods #
+ # #
+ security.declareProtected(ViewManagementScreens, 'manage_overview')
+ manage_overview = DTMLFile('dtml/explainGroupsTool', globals()) # unlike MembershipTool
+ security.declareProtected(ViewManagementScreens, 'manage_config')
+ manage_config = DTMLFile('dtml/configureGroupsTool', globals())
+
+ security.declareProtected(ManagePortal, 'manage_setGroupWorkspacesFolder')
+ def manage_setGroupWorkspacesFolder(self, id='groups', title='Groups', REQUEST=None):
+ """ZMI method for workspace container name set."""
+ self.setGroupWorkspacesFolder(id, title)
+ return self.manage_config(manage_tabs_message="Workspaces folder name set to %s" % id)
+
+ security.declareProtected(ManagePortal, 'manage_setGroupWorkspaceType')
+ def manage_setGroupWorkspaceType(self, type='Folder', REQUEST=None):
+ """ZMI method for workspace type set."""
+ self.setGroupWorkspaceType(type)
+ return self.manage_config(manage_tabs_message="Group Workspaces type set to %s" % type)
+
+ security.declareProtected(ManagePortal, 'manage_setGroupWorkspaceContainerType')
+ def manage_setGroupWorkspaceContainerType(self, type='Folder', REQUEST=None):
+ """ZMI method for workspace type set."""
+ self.setGroupWorkspaceContainerType(type)
+ return self.manage_config(manage_tabs_message="Group Workspaces container type set to %s" % type)
+
+ security.declareProtected(ViewGroups, 'getGroupById')
+ def getGroupById(self, id):
+ """
+ Returns the portal_groupdata-ish object for a group corresponding to this id.
+ """
+ if id==None:
+ return None
+ g = self.acl_users.getGroupByName(id, None)
+ if g is not None:
+ g = self.wrapGroup(g)
+ return g
+
+ security.declareProtected(ViewGroups, 'getGroupsByUserId')
+ def getGroupsByUserId(self, userid):
+ """Return a list of the groups the user corresponding to 'userid' belongs to."""
+ #log("getGroupsByUserId(%s)" % userid)
+ user = self.acl_users.getUser(userid)
+ #log("user '%s' is in groups %s" % (userid, user.getGroups()))
+ if user:
+ groups = user.getGroups() or []
+ else:
+ groups = []
+ return [self.getGroupById(elt) for elt in groups]
+
+ security.declareProtected(ViewGroups, 'listGroups')
+ def listGroups(self):
+ """Return a list of the available portal_groupdata-ish objects."""
+ return [ self.wrapGroup(elt) for elt in self.acl_users.getGroups() ]
+
+ security.declareProtected(ViewGroups, 'listGroupIds')
+ def listGroupIds(self):
+ """Return a list of the available groups' ids as entered (without group prefixes)."""
+ return self.acl_users.getGroupNames()
+
+ security.declareProtected(ViewGroups, 'listGroupNames')
+ def listGroupNames(self):
+ """Return a list of the available groups' ids as entered (without group prefixes)."""
+ return self.acl_users.getGroupNames()
+
+ security.declarePublic("isGroup")
+ def isGroup(self, u):
+ """Test if a user/group object is a group or not.
+ You must pass an object you get earlier with wrapUser() or wrapGroup()
+ """
+ base = aq_base(u)
+ if hasattr(base, "isGroup") and base.isGroup():
+ return 1
+ return 0
+
+ security.declareProtected(View, 'searchForGroups')
+ def searchForGroups(self, REQUEST = {}, **kw):
+ """Return a list of groups meeting certain conditions. """
+ # arguments need to be better refined?
+ if REQUEST:
+ dict = REQUEST
+ else:
+ dict = kw
+
+ name = dict.get('name', None)
+ email = dict.get('email', None)
+ roles = dict.get('roles', None)
+ title = dict.get('title', None)
+ title_or_name = dict.get('title_or_name', None)
+
+ last_login_time = dict.get('last_login_time', None)
+ #is_manager = self.checkPermission('Manage portal', self)
+
+ if name:
+ name = name.strip().lower()
+ if not name:
+ name = None
+ if email:
+ email = email.strip().lower()
+ if not email:
+ email = None
+ if title:
+ title = title.strip().lower()
+ if title_or_name:
+ title_or_name = title_or_name.strip().lower()
+ if not title:
+ title = None
+
+ res = []
+ portal = self.portal_url.getPortalObject()
+ for g in portal.portal_groups.listGroups():
+ #if not (g.listed or is_manager):
+ # continue
+ if name:
+ if (g.getGroupName().lower().find(name) == -1) and (g.getGroupId().lower().find(name) == -1):
+ continue
+ if email:
+ if g.email.lower().find(email) == -1:
+ continue
+ if roles:
+ group_roles = g.getRoles()
+ found = 0
+ for r in roles:
+ if r in group_roles:
+ found = 1
+ break
+ if not found:
+ continue
+ if title:
+ if g.title.lower().find(title) == -1:
+ continue
+ if title_or_name:
+ # first search for title
+ if g.title.lower().find(title_or_name) == -1:
+ # not found, now search for name
+ if (g.getGroupName().lower().find(title_or_name) == -1) and (g.getGroupId().lower().find(title_or_name) == -1):
+ continue
+
+ if last_login_time:
+ if g.last_login_time < last_login_time:
+ continue
+ res.append(g)
+
+ return res
+
+ security.declareProtected(AddGroups, 'addGroup')
+ def addGroup(self, id, roles = [], groups = [], REQUEST=None, *args, **kw):
+ """Create a group, and a group workspace if the toggle is on, with the supplied id, roles, and domains.
+
+ Underlying user folder must support adding users via the usual Zope API.
+ Passwords for groups ARE irrelevant in GRUF."""
+ if id in self.listGroupIds():
+ raise ValueError, "Group '%s' already exists." % (id, )
+ self.acl_users.userFolderAddGroup(id, roles = roles, groups = groups )
+ self.createGrouparea(id)
+ self.getGroupById(id).setProperties(**kw)
+ addGroup = postonly(addGroup)
+
+ security.declareProtected(ManageGroups, 'editGroup')
+ def editGroup(self, id, roles = None, groups = None, REQUEST=None, *args, **kw):
+ """Edit the given group with the supplied password, roles, and domains.
+
+ Underlying user folder must support editing users via the usual Zope API.
+ Passwords for groups seem to be currently irrelevant in GRUF."""
+ self.acl_users.userFolderEditGroup(id, roles = roles, groups = groups, )
+ self.getGroupById(id).setProperties(**kw)
+ editGroup = postonly(editGroup)
+
+ security.declareProtected(DeleteGroups, 'removeGroups')
+ def removeGroups(self, ids, keep_workspaces=0, REQUEST=None):
+ """Remove the group in the provided list (if possible).
+
+ Will by default remove this group's GroupWorkspace if it exists. You may
+ turn this off by specifying keep_workspaces=true.
+ Underlying user folder must support removing users via the usual Zope API."""
+ for gid in ids:
+ gdata = self.getGroupById(gid)
+ gusers = gdata.getGroupMembers()
+ for guser in gusers:
+ gdata.removeMember(guser.id)
+
+ self.acl_users.userFolderDelGroups(ids)
+ gwf = self.getGroupWorkspacesFolder()
+ if not gwf: # _robert_
+ return
+ if not keep_workspaces:
+ for id in ids:
+ if hasattr(aq_base(gwf), id):
+ gwf._delObject(id)
+ removeGroups = postonly(removeGroups)
+
+ security.declareProtected(SetGroupOwnership, 'setGroupOwnership')
+ def setGroupOwnership(self, group, object, REQUEST=None):
+ """Make the object 'object' owned by group 'group' (a portal_groupdata-ish object).
+
+ For GRUF this is easy. Others may have to re-implement."""
+ user = group.getGroup()
+ if user is None:
+ raise ValueError, "Invalid group: '%s'." % (group, )
+ object.changeOwnership(user)
+ object.manage_setLocalRoles(user.getId(), ['Owner'])
+ setGroupOwnership = postonly(setGroupOwnership)
+
+ security.declareProtected(ManagePortal, 'setGroupWorkspacesFolder')
+ def setGroupWorkspacesFolder(self, id="", title=""):
+ """ Set the location of the Group Workspaces folder by id.
+
+ The Group Workspaces Folder contains all the group workspaces, just like the
+ Members folder contains all the member folders.
+
+ If anyone really cares, we can probably make the id work as a path as well,
+ but for the moment it's only an id for a folder in the portal root, just like the
+ corresponding MembershipTool functionality. """
+ self.groupworkspaces_id = id.strip()
+ self.groupworkspaces_title = title
+
+ security.declareProtected(ManagePortal, 'getGroupWorkspacesFolderId')
+ def getGroupWorkspacesFolderId(self):
+ """ Get the Group Workspaces folder object's id.
+
+ The Group Workspaces Folder contains all the group workspaces, just like the
+ Members folder contains all the member folders. """
+ return self.groupworkspaces_id
+
+ security.declareProtected(ManagePortal, 'getGroupWorkspacesFolderTitle')
+ def getGroupWorkspacesFolderTitle(self):
+ """ Get the Group Workspaces folder object's title.
+ """
+ return self.groupworkspaces_title
+
+ security.declarePublic('getGroupWorkspacesFolder')
+ def getGroupWorkspacesFolder(self):
+ """ Get the Group Workspaces folder object.
+
+ The Group Workspaces Folder contains all the group workspaces, just like the
+ Members folder contains all the member folders. """
+ parent = self.aq_inner.aq_parent
+ folder = getattr(parent, self.getGroupWorkspacesFolderId(), None)
+ return folder
+
+ security.declareProtected(ManagePortal, 'toggleGroupWorkspacesCreation')
+ def toggleGroupWorkspacesCreation(self, REQUEST=None):
+ """ Toggles the flag for creation of a GroupWorkspaces folder upon creation of the group. """
+ if not hasattr(self, 'groupWorkspacesCreationFlag'):
+ self.groupWorkspacesCreationFlag = 0
+
+ self.groupWorkspacesCreationFlag = not self.groupWorkspacesCreationFlag
+
+ m = self.groupWorkspacesCreationFlag and 'turned on' or 'turned off'
+
+ return self.manage_config(manage_tabs_message="Workspaces creation %s" % m)
+
+ security.declareProtected(ManagePortal, 'getGroupWorkspacesCreationFlag')
+ def getGroupWorkspacesCreationFlag(self):
+ """Return the (boolean) flag indicating whether the Groups Tool will create a group workspace
+ upon the creation of the group (if one doesn't exist already). """
+ return self.groupWorkspacesCreationFlag
+
+ security.declareProtected(AddGroups, 'createGrouparea')
+ def createGrouparea(self, id):
+ """Create a space in the portal for the given group, much like member home
+ folders."""
+ parent = self.aq_inner.aq_parent
+ workspaces = self.getGroupWorkspacesFolder()
+ pt = getToolByName( self, 'portal_types' )
+
+ if id and self.getGroupWorkspacesCreationFlag():
+ if workspaces is None:
+ # add GroupWorkspaces folder
+ pt.constructContent(
+ type_name = self.getGroupWorkspaceContainerType(),
+ container = parent,
+ id = self.getGroupWorkspacesFolderId(),
+ )
+ workspaces = self.getGroupWorkspacesFolder()
+ workspaces.setTitle(self.getGroupWorkspacesFolderTitle())
+ workspaces.setDescription("Container for " + self.getGroupWorkspacesFolderId())
+ # how about ownership?
+
+ # this stuff like MembershipTool...
+ portal_catalog = getToolByName( self, 'portal_catalog' )
+ portal_catalog.unindexObject(workspaces) # unindex GroupWorkspaces folder
+ workspaces._setProperty('right_slots', (), 'lines')
+
+ if workspaces is not None and not hasattr(workspaces.aq_base, id):
+ # add workspace to GroupWorkspaces folder
+ pt.constructContent(
+ type_name = self.getGroupWorkspaceType(),
+ container = workspaces,
+ id = id,
+ )
+ space = self.getGroupareaFolder(id)
+ space.setTitle("%s workspace" % id)
+ space.setDescription("Container for objects shared by this group")
+
+ if hasattr(space, 'setInitialGroup'):
+ # GroupSpaces can have their own policies regarding the group
+ # that they are created for.
+ user = self.getGroupById(id).getGroup()
+ if user is not None:
+ space.setInitialGroup(user)
+ else:
+ space.manage_delLocalRoles(space.users_with_local_role('Owner'))
+ self.setGroupOwnership(self.getGroupById(id), space)
+
+ # Hook to allow doing other things after grouparea creation.
+ notify_script = getattr(workspaces, 'notifyGroupAreaCreated', None)
+ if notify_script is not None:
+ notify_script()
+
+ # Re-indexation
+ portal_catalog = getToolByName( self, 'portal_catalog' )
+ portal_catalog.reindexObject(space)
+
+ security.declareProtected(ManagePortal, 'getGroupWorkspaceType')
+ def getGroupWorkspaceType(self):
+ """Return the Type (as in TypesTool) to make the GroupWorkspace."""
+ return self.groupWorkspaceType
+
+ security.declareProtected(ManagePortal, 'setGroupWorkspaceType')
+ def setGroupWorkspaceType(self, type):
+ """Set the Type (as in TypesTool) to make the GroupWorkspace."""
+ self.groupWorkspaceType = type
+
+ security.declareProtected(ManagePortal, 'getGroupWorkspaceContainerType')
+ def getGroupWorkspaceContainerType(self):
+ """Return the Type (as in TypesTool) to make the GroupWorkspace."""
+ return self.groupWorkspaceContainerType
+
+ security.declareProtected(ManagePortal, 'setGroupWorkspaceContainerType')
+ def setGroupWorkspaceContainerType(self, type):
+ """Set the Type (as in TypesTool) to make the GroupWorkspace."""
+ self.groupWorkspaceContainerType = type
+
+ security.declarePublic('getGroupareaFolder')
+ def getGroupareaFolder(self, id=None, verifyPermission=0):
+ """Returns the object of the group's work area."""
+ if id is None:
+ group = self.getAuthenticatedMember()
+ if not hasattr(member, 'getGroupId'):
+ return None
+ id = group.getGroupId()
+ workspaces = self.getGroupWorkspacesFolder()
+ if workspaces:
+ try:
+ folder = workspaces[id]
+ if verifyPermission and not _checkPermission('View', folder):
+ # Don't return the folder if the user can't get to it.
+ return None
+ return folder
+ except KeyError: pass
+ return None
+
+ security.declarePublic('getGroupareaURL')
+ def getGroupareaURL(self, id=None, verifyPermission=0):
+ """Returns the full URL to the group's work area."""
+ ga = self.getGroupareaFolder(id, verifyPermission)
+ if ga is not None:
+ return ga.absolute_url()
+ else:
+ return None
+
+ security.declarePrivate('wrapGroup')
+ def wrapGroup(self, g, wrap_anon=0):
+ ''' Sets up the correct acquisition wrappers for a group
+ object and provides an opportunity for a portal_memberdata
+ tool to retrieve and store member data independently of
+ the user object.
+ '''
+ b = getattr(g, 'aq_base', None)
+ if b is None:
+ # u isn't wrapped at all. Wrap it in self.acl_users.
+ b = g
+ g = g.__of__(self.acl_users)
+ if (b is nobody and not wrap_anon) or hasattr(b, 'getMemberId'):
+ # This user is either not recognized by acl_users or it is
+ # already registered with something that implements the
+ # member data tool at least partially.
+ return g
+
+ parent = self.aq_inner.aq_parent
+ base = getattr(parent, 'aq_base', None)
+ if hasattr(base, 'portal_groupdata'):
+ # Get portal_groupdata to do the wrapping.
+ Log(LOG_DEBUG, "parent", parent)
+ gd = getToolByName(parent, 'portal_groupdata')
+ Log(LOG_DEBUG, "group data", gd)
+ try:
+ #log("wrapping group %s" % g)
+ portal_group = gd.wrapGroup(g)
+ return portal_group
+ except ConflictError:
+ raise
+ except:
+ import logging
+ logger = logging.getLogger('GroupUserFolder.GroupsTool')
+ logger.exception('Error during wrapGroup')
+ # Failed.
+ return g
+
+InitializeClass(GroupsTool)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+## Copyright (c) 2003 The Connexions Project, All Rights Reserved
+## initially written by J Cameron Cooper, 11 June 2003
+## concept with Brent Hendricks, George Runyan
+"""
+Basic usergroup tool.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: GroupsToolPermissions.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+# BBB CMF < 1.5
+try:
+ from Products.CMFCore.permissions import *
+except ImportError:
+ from Products.CMFCore.CMFCorePermissions import *
+
+AddGroups = 'Add Groups'
+setDefaultRoles(AddGroups, ('Manager',))
+
+ManageGroups = 'Manage Groups'
+setDefaultRoles(ManageGroups, ('Manager',))
+
+ViewGroups = 'View Groups'
+setDefaultRoles(ViewGroups, ('Manager', 'Owner', 'Member'))
+
+DeleteGroups = 'Delete Groups'
+setDefaultRoles(DeleteGroups, ('Manager', ))
+
+SetGroupOwnership = 'Set Group Ownership'
+setDefaultRoles(SetGroupOwnership, ('Manager', 'Owner'))
--- /dev/null
+HOW TO INSTALL GRUF?
+
+ GRUF installs just like any other Zope product. Just untar it in your Products directory,
+ restart Zope, and you're done.
+
+HOW TO USE GRUF?
+
+ To enjoy groups within Zope, you just have to instansiate a GroupUserFolder instead of your
+ UserFolder. GRUF creates two default acl_users for you inside itself (one for Users and one
+ for Groups. see README.txt for technical explanation) but you can remove them and replace
+ them by other kind of User Folders: LDAPUserFolder, SQLUserFolder, SimpleUserFolder,
+ or whatever suits your needs.
+
+PLONE INSTALLATION
+
+ See README-Plone file for explanation on Plone installation.
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: Installation.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+
+from cStringIO import StringIO
+import string
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.TypesTool import ContentFactoryMetadata
+from Products.CMFCore.DirectoryView import addDirectoryViews
+from Products.CMFPlone.migrations.migration_util import safeEditProperty
+
+class Installation:
+ def __init__(self, root):
+ self.root=root
+ self.out=StringIO()
+ self.typesTool = getToolByName(self.root, 'portal_types')
+ self.skinsTool = getToolByName(self.root, 'portal_skins')
+ self.portal_properties = getToolByName(self.root, 'portal_properties')
+ self.navigation_properties = self.portal_properties.navigation_properties
+ self.form_properties = self.portal_properties.form_properties
+
+ def report(self):
+ self.out.write('Installation completed.\n')
+ return self.out.getvalue()
+
+ def setupTools(self, product_name, tools):
+ addTool = self.root.manage_addProduct[product_name].manage_addTool
+ for tool, title in tools:
+ found = 0
+ for obj in self.root.objectValues():
+ if obj.meta_type == tool:
+ found = 1
+ if not found:
+ addTool(tool, None)
+
+ found = 0
+ root=self.root
+ for obj in root.objectValues():
+ if obj.meta_type == tool:
+ obj.title=title
+ self.out.write("Added '%s' tool.\n" % (tool,))
+ found = 1
+ if not found:
+ self.out.write("Couldn't add '%s' tool.\n" % (tool,))
+
+ def installSubSkin(self, skinFolder):
+ """ Install a subskin, i.e. a folder/directoryview.
+ """
+ for skin in self.skinsTool.getSkinSelections():
+ path = self.skinsTool.getSkinPath(skin)
+ path = map( string.strip, string.split( path,',' ) )
+ if not skinFolder in path:
+ try:
+ path.insert( path.index( 'custom')+1, skinFolder )
+ except ValueError:
+ path.append(skinFolder)
+ path = string.join( path, ', ' )
+ self.skinsTool.addSkinSelection( skin, path )
+ self.out.write('Subskin successfully installed into %s.\n' % skin)
+ else:
+ self.out.write('*** Subskin was already installed into %s.\n' % skin)
+
+ def setupCustomModelsSkin(self, skin_name):
+ """ Install custom skin folder
+ """
+ try:
+ self.skinsTool.manage_addProduct['OFSP'].manage_addFolder(skin_name + 'CustomModels')
+ except:
+ self.out.write('*** Skin %sCustomModels already existed in portal_skins.\n' % skin_name)
+ self.installSubSkin('%sCustomModels' % skin_name)
+
+ def setupTypesandSkins(self, fti_list, skin_name, install_globals):
+ """
+ setup of types and skins
+ """
+
+ # Former types deletion (added by PJG)
+ for f in fti_list:
+ if f['id'] in self.typesTool.objectIds():
+ self.out.write('*** Object "%s" already existed in the types tool => deleting\n' % (f['id']))
+ self.typesTool._delObject(f['id'])
+
+ # Type re-creation
+ for f in fti_list:
+ # Plone1 : if cmfformcontroller is not available and plone1_action key is defined,
+ # use this key instead of the regular 'action' key.
+ if (not self.hasFormController()) and f.has_key('plone1_action'):
+ f['action'] = f['plone1_action']
+
+ # Regular FTI processing
+ cfm = apply(ContentFactoryMetadata, (), f)
+ self.typesTool._setObject(f['id'], cfm)
+ self.out.write('Type "%s" registered with the types tool\n' % (f['id']))
+
+ # Install de chaque nouvelle subskin/layer
+ try:
+ addDirectoryViews(self.skinsTool, 'skins', install_globals)
+ self.out.write( "Added directory views to portal_skins.\n" )
+ except:
+ self.out.write( '*** Unable to add directory views to portal_skins.\n')
+
+ # Param de chaque nouvelle subskin/layer
+ self.installSubSkin(skin_name)
+
+ def isPlone2(self,):
+ """
+ isPlone2(self,) => return true if we're using Plone2 ! :-)
+ """
+ return self.hasFormController()
+
+ def hasFormController(self,):
+ """
+ hasFormController(self,) => Return 1 if CMFFC is available
+ """
+ if 'portal_form_controller' in self.root.objectIds():
+ return 1
+ else:
+ return None
+
+ def addFormValidators(self, mapping):
+ """
+ Adds the form validators.
+ DON'T ADD ANYTHING IF CMFFORMCONTROLLER IS INSTALLED
+ """
+ # Plone2 management
+ if self.hasFormController():
+ return
+ for (key, value) in mapping:
+ safeEditProperty(self.form_properties, key, value)
+
+ def addNavigationTransitions(self, transitions):
+ """
+ Adds Navigation Transitions in portal properties
+ """
+ # Plone2 management
+ if self.hasFormController():
+ return
+ for (key, value) in transitions:
+ safeEditProperty(self.navigation_properties, key, value)
+
+ def setPermissions(self, perms_list):
+ """
+ setPermissions(self) => Set standard permissions / roles
+ """
+ # As a default behavior, newly-created permissions are granted to owner and manager.
+ # To change this, just comment this code and grab back the code commented below to
+ # make it suit your needs.
+ for perm in perms_list:
+ self.root.manage_permission(
+ perm,
+ ('Manager', 'Owner'),
+ acquire = 1
+ )
+ self.out.write("Reseted default permissions\n")
+
+ def installMessageCatalog(self, plone, prodglobals, domain, poPrefix):
+ """Sets up the a message catalog for this product
+ according to the available languages in both:
+ - .pot files in the "i18n" folder of this product
+ - MessageCatalog available for this domain
+ Typical use, create below this function:
+ def installCatalog(self):
+ installMessageCatalog(self, Products.MyProduct, 'mydomain', 'potfile_')
+ return
+ This assumes that you set the domain 'mydomain' in 'translation_service'
+ and the .../Products/YourProduct/i18n/potfile_en.po (...) contain your messages.
+
+ @param plone: the plone site
+ @type plone: a 'Plone site' object
+ @param prodglobals: see PloneSkinRegistrar.__init__
+ @param domain: the domain nick in Plone 'translation_service'
+ @type domain: string or None for the default domain
+ (you shouldn't use the default domain)
+ @param poPrefix: .po files to use start with that prefix.
+ i.e. use 'foo_' to install words from 'foo_fr.po', 'foo_en.po' (...)
+ @type poPrefix: string
+ """
+
+ installInfo = (
+ "!! I18N INSTALLATION CANCELED !!\n"
+ "It seems that your Plone instance does not have the i18n features installed correctly.\n"
+ "You should have a 'translation_service' object in your Plone root.\n"
+ "This object should have the '%(domain)s' domain registered and associated\n"
+ "with an **existing** MessageCatalog object.\n"
+ "Fix all this first and come back here." % locals())
+ #
+ # Find Plone i18n resources
+ #
+ try:
+ ts = getattr(plone, 'translation_service')
+ except AttributeError, e:
+ return installInfo
+ found = 0
+ for nick, path in ts.getDomainInfo():
+ if nick == domain:
+ found = 1
+ break
+ if not found:
+ return installInfo
+ try:
+ mc = ts.restrictedTraverse(path)
+ except (AttributeError, KeyError), e:
+ return installInfo
+ self.out.write("Installing I18N messages into '%s'\n" % '/'.join(mc.getPhysicalPath()))
+ enabledLangs = [nick for nick, lang in mc.get_languages_tuple()]
+ self.out.write("This MessageCatalog has %s languages enabled.\n" % ", ".join(enabledLangs))
+ #
+ # Find .po files
+ #
+ i18nPath = os.path.join(prodglobals['__path__'][0], 'i18n')
+ poPtn = os.path.join(i18nPath, poPrefix + '*.po')
+ poFiles = glob.glob(poPtn)
+ rxFindLanguage = re.compile(poPrefix +r'(.*)\.po')
+ poRsrc = {}
+ for file in poFiles:
+ k = rxFindLanguage.findall(file)[0]
+ poRsrc[k] = file
+ self.out.write("This Product provides messages for %s languages.\n" % ", ".join(poRsrc.keys()))
+ for lang in enabledLangs:
+ if poRsrc.has_key(lang):
+ self.out.write("Adding support for language %s.\n" % lang)
+ fh = open(poRsrc[lang])
+ mc.manage_import(lang, fh.read())
+ fh.close()
+ self.out.write("Done !")
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: LDAPGroupFolder.py 587 2008-07-31 09:20:06Z pin $
+__docformat__ = 'restructuredtext'
+
+import time, traceback
+
+# Zope imports
+from Globals import DTMLFile, InitializeClass
+from Acquisition import aq_base
+from AccessControl import ClassSecurityInfo
+from AccessControl.User import SimpleUser
+from AccessControl.Permissions import view_management_screens, manage_users
+from OFS.SimpleItem import SimpleItem
+from DateTime import DateTime
+
+from Products.GroupUserFolder import postonly
+import GroupUserFolder
+
+from global_symbols import *
+
+# LDAPUserFolder package imports
+from Products.LDAPUserFolder.SimpleCache import SimpleCache
+
+addLDAPGroupFolderForm = DTMLFile('dtml/addLDAPGroupFolder', globals())
+
+
+class LDAPGroupFolder(SimpleItem):
+ """ """
+ security = ClassSecurityInfo()
+
+ meta_type = 'LDAPGroupFolder'
+ id = 'acl_users'
+
+ isPrincipiaFolderish=1
+ isAUserFolder=1
+
+ manage_options=(
+ ({'label' : 'Groups', 'action' : 'manage_main',},)
+ + SimpleItem.manage_options
+ )
+
+ security.declareProtected(view_management_screens, 'manage_main')
+ manage_main = DTMLFile('dtml/groups', globals())
+
+
+ def __setstate__(self, v):
+ """ """
+ LDAPGroupFolder.inheritedAttribute('__setstate__')(self, v)
+ self._cache = SimpleCache()
+ self._cache.setTimeout(600)
+ self._cache.clear()
+
+ def __init__( self, title, luf=''):
+ """ """
+ self._luf = luf
+ self._cache = SimpleCache()
+ self._cache.setTimeout(600)
+ self._cache.clear()
+
+ security.declarePrivate(manage_users, 'getGRUF')
+ def getGRUF(self):
+ """ """
+ return self.aq_parent.aq_parent
+
+
+ security.declareProtected(manage_users, 'getLUF')
+ def getLUF(self):
+ """ """
+ s = self.getGRUF().getUserSource(self._luf)
+ if getattr(s, 'meta_type', None) != "LDAPUserFolder":
+ # whoops, we moved LDAPUF... let's try to find it back
+ Log(LOG_WARNING, "LDAPUserFolder moved. Trying to find it back.")
+ s = None
+ for src in self.getGRUF().listUserSources():
+ if src.meta_type == "LDAPUserFolder":
+ self._luf = src.getPhysicalPath()[-2]
+ s = src
+ break
+ if not s:
+ raise RuntimeError, "You must change your groups source in GRUF if you do not have a LDAPUserFolder as a users source."
+ return s
+
+
+ security.declareProtected(manage_users, 'getGroups')
+ def getGroups(self, dn='*', attr=None, pwd=''):
+ """ """
+ return self.getLUF().getGroups(dn, attr, pwd)
+
+
+ security.declareProtected(manage_users, 'getGroupType')
+ def getGroupType(self, group_dn):
+ """ """
+ return self.getLUF().getGroupType(group_dn)
+
+ security.declareProtected(manage_users, 'getGroupMappings')
+ def getGroupMappings(self):
+ """ """
+ return self.getLUF().getGroupMappings()
+
+ security.declareProtected(manage_users, 'manage_addGroupMapping')
+ def manage_addGroupMapping(self, group_name, role_name, REQUEST=None):
+ """ """
+ self._cache.remove(group_name)
+ self.getLUF().manage_addGroupMapping(group_name, role_name, None)
+
+ if REQUEST:
+ msg = 'Added LDAP group to Zope role mapping: %s -> %s' % (
+ group_name, role_name)
+ return self.manage_main(manage_tabs_message=msg)
+ manage_addGroupMapping = postonly(manage_addGroupMapping)
+
+ security.declareProtected(manage_users, 'manage_deleteGroupMappings')
+ def manage_deleteGroupMappings(self, group_names, REQUEST=None):
+ """ Delete mappings from LDAP group to Zope role """
+ self._cache.clear()
+ self.getLUF().manage_deleteGroupMappings(group_names, None)
+ if REQUEST:
+ msg = 'Deleted LDAP group to Zope role mapping for: %s' % (
+ ', '.join(group_names))
+ return self.manage_main(manage_tabs_message=msg)
+ manage_deleteGroupMappings = postonly(manage_deleteGroupMappings)
+
+ security.declareProtected(manage_users, 'manage_addGroup')
+ def manage_addGroup( self
+ , newgroup_name
+ , newgroup_type='groupOfUniqueNames'
+ , REQUEST=None
+ ):
+ """Add a new group in groups_base.
+ """
+ self.getLUF().manage_addGroup(newgroup_name, newgroup_type, None)
+
+ if REQUEST:
+ msg = 'Added new group %s' % (newgroup_name)
+ return self.manage_main(manage_tabs_message=msg)
+ manage_addGroup = postonly(manage_addGroup)
+
+ security.declareProtected(manage_users, 'manage_deleteGroups')
+ def manage_deleteGroups(self, dns=[], REQUEST=None):
+ """ Delete groups from groups_base """
+ self.getLUF().manage_deleteGroups(dns, None)
+ self._cache.clear()
+
+ if REQUEST:
+ msg = 'Deleted group(s):<br> %s' % '<br>'.join(dns)
+ return self.manage_main(manage_tabs_message=msg)
+ manage_deleteGroups = postonly(manage_deleteGroups)
+
+ security.declareProtected(manage_users, 'getUser')
+ def getUser(self, name):
+ """ """
+ # Prevent locally stored groups
+ luf = self.getLUF()
+ if luf._local_groups:
+ return []
+
+ # Get the group from the cache
+ user = self._cache.get(name, '')
+ if user:
+ return user
+
+ # Scan groups to find the proper user.
+ # THIS MAY BE EXPENSIVE AND HAS TO BE OPTIMIZED...
+ grps = self.getLUF().getGroups()
+ valid_roles = self.userFolderGetRoles()
+ dn = None
+ for n, g_dn in grps:
+ if n == name:
+ dn = g_dn
+ break
+ if not dn:
+ return None
+
+ # Current mapping
+ roles = self.getLUF()._mapRoles([name])
+
+ # Nested groups
+ groups = list(self.getLUF().getGroups(dn=dn, attr='cn', ))
+ roles.extend(self.getLUF()._mapRoles(groups))
+
+ # !!! start test
+ Log(LOG_DEBUG, name, "roles", groups, roles)
+ Log(LOG_DEBUG, name, "mapping", getattr(self.getLUF(), '_groups_mappings', {}))
+ # !!! end test
+
+ actual_roles = []
+ for r in roles:
+ if r in valid_roles:
+ actual_roles.append(r)
+ elif "%s%s" % (GROUP_PREFIX, r) in valid_roles:
+ actual_roles.append("%s%s" % (GROUP_PREFIX, r))
+ Log(LOG_DEBUG, name, "actual roles", actual_roles)
+ user = GroupUser(n, '', actual_roles, [])
+ self._cache.set(name, user)
+ return user
+
+ security.declareProtected(manage_users, 'getUserNames')
+ def getUserNames(self):
+ """ """
+ Log(LOG_DEBUG, "getUserNames", )
+ LogCallStack(LOG_DEBUG)
+ # Prevent locally stored groups
+ luf = self.getLUF()
+ if luf._local_groups:
+ return []
+ return [g[0] for g in luf.getGroups()]
+
+ security.declareProtected(manage_users, 'getUsers')
+ def getUsers(self, authenticated=1):
+ """ """
+ # Prevent locally stored groups
+ luf = self.getLUF()
+ if luf._local_groups:
+ return []
+
+ data = []
+
+ grps = self.getLUF().getGroups()
+ valid_roles = self.userFolderGetRoles()
+ for n, dn in grps:
+ # Group mapping
+ roles = self.getLUF()._mapRoles([n])
+
+ # computation
+ actual_roles = []
+ for r in roles:
+ if r in valid_roles:
+ actual_roles.append(r)
+ elif "%s%s" % (GROUP_PREFIX, r) in valid_roles:
+ actual_roles.append("%s%s" % (GROUP_PREFIX, r))
+ user = GroupUser(n, '', actual_roles, [])
+ data.append(user)
+
+ return data
+
+ security.declarePrivate('_doAddUser')
+ def _doAddUser(self, name, password, roles, domains, **kw):
+ """WARNING: If a role with exists with the same name as the group, we do not add
+ the group mapping for it, but we create it as if it were a Zope ROLE.
+ Ie. it's not possible to have a GRUF Group name = a Zope role name, BUT,
+ with this system, it's possible to differenciate between LDAP groups and LDAP roles.
+ """
+ self.getLUF().manage_addGroup(name)
+ self.manage_addGroupMapping(name, "group_" + name, None, )
+ self._doChangeUser(name, password, roles, domains, **kw)
+
+ security.declarePrivate('_doDelUsers')
+ def _doDelUsers(self, names):
+ dns = []
+ luf = self.getLUF()
+ for g_name, dn in luf.getGroups():
+ if g_name in names:
+ dns.append(dn)
+ self._cache.clear()
+ return luf.manage_deleteGroups(dns)
+
+ security.declarePrivate('_doChangeUser')
+ def _doChangeUser(self, name, password, roles, domains, **kw):
+ """
+ This is used to change the groups (especially their roles).
+
+ [ THIS TEXT IS OUTDATED :
+ WARNING: If a ZOPE role with the same name as the GRUF group exists,
+ we do not add the group mapping for it, but we create it as if it were a Zope ROLE.
+ Ie. it's not possible to have a GRUF Group name = a Zope role name, BUT,
+ with this system, it's possible to differenciate between LDAP groups and LDAP roles.
+ ]
+ """
+ luf = self.getLUF()
+ self._cache.remove(name)
+
+ # Get group DN
+ dn = None
+ for g_name, g_dn in luf.getGroups():
+ if g_name == name:
+ dn = g_dn
+ break
+ if not dn:
+ raise ValueError, "Invalid LDAP group: '%s'" % (name, )
+
+ # Edit group mappings
+## if name in self.aq_parent.valid_roles():
+## # This is, in fact, a role
+## self.getLUF().manage_addGroupMapping(name, name)
+## else:
+## # This is a group -> we set it as a group
+## self.getLUF().manage_addGroupMapping(name, self.getGroupPrefix() + name)
+
+ # Change roles
+ if luf._local_groups:
+ luf.manage_editUserRoles(dn, roles)
+ else:
+ # We have to transform roles into group dns: transform them as a dict
+ role_dns = []
+ all_groups = luf.getGroups()
+ all_roles = luf.valid_roles()
+ groups = {}
+ for g in all_groups:
+ groups[g[0]] = g[1]
+
+ # LDAPUF < 2.4Beta3 adds possibly invalid roles to the user roles
+ # (for example, adding the cn of a group additionnaly to the mapped zope role).
+ # So we must remove from our 'roles' list all roles which are prefixed by group prefix
+ # but are not actually groups.
+ # If a group has the same name as a role, we assume that it should be a _role_.
+ # We should check against group/role mapping here, but... well... XXX TODO !
+ # See "HERE IT IS" comment below.
+
+ # Scan roles we are asking for to manage groups correctly
+ for role in roles:
+ if not role in all_roles:
+ continue # Do not allow propagation of invalid roles
+ if role.startswith(GROUP_PREFIX):
+ role = role[GROUP_PREFIX_LEN:] # Remove group prefix : groups are stored WITHOUT prefix in LDAP
+ if role in all_roles:
+ continue # HERE IT IS
+ r = groups.get(role, None)
+ if not r:
+ Log(LOG_WARNING, "LDAP Server doesn't provide a '%s' group (asked for user '%s')." % (role, name, ))
+ continue
+ role_dns.append(r)
+
+ # Perform the change
+ luf.manage_editGroupRoles(dn, role_dns)
+
+
+
+def manage_addLDAPGroupFolder( self, title = '', luf='', REQUEST=None):
+ """ """
+ this_folder = self.this()
+
+ if hasattr(aq_base(this_folder), 'acl_users') and REQUEST is not None:
+ msg = 'This+object+already+contains+a+User+Folder'
+
+ else:
+ # Try to guess where is LUF
+ if not luf:
+ for src in this_folder.listUserSources():
+ if src.meta_type == "LDAPUserFolder":
+ luf = src.aq_parent.getId()
+
+ # No LUF found : error
+ if not luf:
+ raise KeyError, "You must be within GRUF with a LDAPUserFolder as one of your user sources."
+
+ n = LDAPGroupFolder( title, luf )
+
+ this_folder._setObject('acl_users', n)
+ this_folder.__allow_groups__ = self.acl_users
+
+ msg = 'Added+LDAPGroupFolder'
+
+ # return to the parent object's manage_main
+ if REQUEST:
+ url = REQUEST['URL1']
+ qs = 'manage_tabs_message=%s' % msg
+ REQUEST.RESPONSE.redirect('%s/manage_main?%s' % (url, qs))
+
+
+InitializeClass(LDAPGroupFolder)
+
+
+class GroupUser(SimpleUser):
+ """ """
+
+ def __init__(self, name, password, roles, domains):
+ SimpleUser.__init__(self, name, password, roles, domains)
+ self._created = time.time()
+
+ def getCreationTime(self):
+ """ """
+ return DateTime(self._created)
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: LDAPUserFolderAdapter.py 587 2008-07-31 09:20:06Z pin $
+__docformat__ = 'restructuredtext'
+
+
+from global_symbols import *
+from Products.GroupUserFolder import postonly
+
+
+# These mandatory attributes are required by LDAP schema.
+# They will be filled with user name as a default value.
+# You have to provide a gruf_ldap_required_fields python script
+# in your Plone's skins if you want to override this.
+MANDATORY_ATTRIBUTES = ("sn", "cn", )
+
+
+def _doAddUser(self, name, password, roles, domains, **kw):
+ """
+ Special user adding method for use with LDAPUserFolder.
+ This will ensure parameters are correct for LDAP management
+ """
+ kwargs = {} # We will pass this dict
+ attrs = {}
+
+ # Get gruf_ldap_required_fields result and fill in mandatory stuff
+ if hasattr(self, "gruf_ldap_required_fields"):
+ attrs = self.gruf_ldap_required_fields(login = name)
+ else:
+ for attr in MANDATORY_ATTRIBUTES:
+ attrs[attr] = name
+ kwargs.update(attrs)
+
+ # We assume that name is rdn attribute
+ rdn_attr = self._rdnattr
+ kwargs[rdn_attr] = name
+
+ # Manage password(s)
+ kwargs['user_pw'] = password
+ kwargs['confirm_pw'] = password
+
+ # Mangle roles
+ kwargs['user_roles'] = self._mangleRoles(name, roles)
+
+ # Delegate to LDAPUF default method
+ msg = self.manage_addUser(kwargs = kwargs)
+ if msg:
+ raise RuntimeError, msg
+
+
+def _doDelUsers(self, names):
+ """
+ Remove a bunch of users from LDAP.
+ We have to call manage_deleteUsers but, before, we need to find their dn.
+ """
+ dns = []
+ for name in names:
+ dns.append(self._find_user_dn(name))
+
+ self.manage_deleteUsers(dns)
+
+
+def _find_user_dn(self, name):
+ """
+ Convert a name to an LDAP dn
+ """
+ # Search records matching name
+ login_attr = self._login_attr
+ v = self.findUser(search_param = login_attr, search_term = name)
+
+ # Filter to keep exact matches only
+ v = filter(lambda x: x[login_attr] == name, v)
+
+ # Now, decide what to do
+ l = len(v)
+ if not l:
+ # Invalid name
+ raise "Invalid user name: '%s'" % (name, )
+ elif l > 1:
+ # Several records... don't know how to handle
+ raise "Duplicate user name for '%s'" % (name, )
+ return v[0]['dn']
+
+
+def _mangleRoles(self, name, roles):
+ """
+ Return role_dns for this user
+ """
+ # Local groups => the easiest part
+ if self._local_groups:
+ return roles
+
+ # We have to transform roles into group dns: transform them as a dict
+ role_dns = []
+ all_groups = self.getGroups()
+ all_roles = self.valid_roles()
+ groups = {}
+ for g in all_groups:
+ groups[g[0]] = g[1]
+
+ # LDAPUF does the mistake of adding possibly invalid roles to the user roles
+ # (for example, adding the cn of a group additionnaly to the mapped zope role).
+ # So we must remove from our 'roles' list all roles which are prefixed by group prefix
+ # but are not actually groups.
+ # See http://www.dataflake.org/tracker/issue_00376 for more information on that
+ # particular issue.
+ # If a group has the same name as a role, we assume that it should be a _role_.
+ # We should check against group/role mapping here, but... well... XXX TODO !
+ # See "HERE IT IS" comment below.
+
+ # Scan roles we are asking for to manage groups correctly
+ for role in roles:
+ if not role in all_roles:
+ continue # Do not allow propagation of invalid roles
+ if role.startswith(GROUP_PREFIX):
+ role = role[GROUP_PREFIX_LEN:] # Remove group prefix : groups are stored WITHOUT prefix in LDAP
+ if role in all_roles:
+ continue # HERE IT IS
+ r = groups.get(role, None)
+ if not r:
+ Log(LOG_WARNING, "LDAP Server doesn't provide a '%s' group (required for user '%s')." % (role, name, ))
+ else:
+ role_dns.append(r)
+
+ return role_dns
+
+
+def _doChangeUser(self, name, password, roles, domains, **kw):
+ """
+ Update a user
+ """
+ # Find the dn at first
+ dn = self._find_user_dn(name)
+
+ # Change password
+ if password is not None:
+ if password == '':
+ raise ValueError, "Password must not be empty for LDAP users."
+ self.manage_editUserPassword(dn, password)
+
+ # Perform role change
+ self.manage_editUserRoles(dn, self._mangleRoles(name, roles))
+
+ # (No domain management with LDAP.)
+
+
+def manage_editGroupRoles(self, user_dn, role_dns=[], REQUEST=None):
+ """ Edit the roles (groups) of a group """
+ from Products.LDAPUserFolder.utils import GROUP_MEMBER_MAP
+ try:
+ from Products.LDAPUserFolder.LDAPDelegate import ADD, DELETE
+ except ImportError:
+ # Support for LDAPUserFolder >= 2.6
+ ADD = self._delegate.ADD
+ DELETE = self._delegate.DELETE
+
+ msg = ""
+
+## Log(LOG_DEBUG, "assigning", role_dns, "to", user_dn)
+ all_groups = self.getGroups(attr='dn')
+ cur_groups = self.getGroups(dn=user_dn, attr='dn')
+ group_dns = []
+ for group in role_dns:
+ if group.find('=') == -1:
+ group_dns.append('cn=%s,%s' % (group, self.groups_base))
+ else:
+ group_dns.append(group)
+
+ if self._local_groups:
+ if len(role_dns) == 0:
+ del self._groups_store[user_dn]
+ else:
+ self._groups_store[user_dn] = role_dns
+
+ else:
+ for group in all_groups:
+ member_attr = GROUP_MEMBER_MAP.get(self.getGroupType(group))
+
+ if group in cur_groups and group not in group_dns:
+ action = DELETE
+ elif group in group_dns and group not in cur_groups:
+ action = ADD
+ else:
+ action = None
+ if action is not None:
+ msg = self._delegate.modify(
+ group
+ , action
+ , {member_attr : [user_dn]}
+ )
+## Log(LOG_DEBUG, "group", group, "subgroup", user_dn, "result", msg)
+
+ if msg:
+ raise RuntimeError, msg
+manage_editGroupRoles = postonly(manage_editGroupRoles)
--- /dev/null
+Zope Public License (ZPL) Version 2.0
+-----------------------------------------------
+
+This software is Copyright (c) Ingeniweb (tm) and
+Contributors. All rights reserved.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the above
+ copyright notice, this list of conditions, and the following
+ disclaimer.
+
+2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+3. The name Ingeniweb (tm) must not be used to
+ endorse or promote products derived from this software
+ without prior written permission from Ingeniweb.
+
+4. The right to distribute this software or to use it for
+ any purpose does not give you the right to use Servicemarks
+ (sm) or Trademarks (tm) of Ingeniweb.
+
+5. If any files are modified, you must cause the modified
+ files to carry prominent notices stating that you changed
+ the files and the date of any change.
+
+Disclaimer
+
+ THIS SOFTWARE IS PROVIDED BY INGENIWEB ``AS IS''
+ AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ NO EVENT SHALL INGENIWEB OR ITS CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
+
+
+This software consists of contributions made by Ingeniweb
+and many individuals on behalf of Ingeniweb.
+Specific attributions are listed in the
+accompanying credits file.
\ No newline at end of file
--- /dev/null
+Zope Public License (ZPL) Version 2.0
+-----------------------------------------------
+
+This software is Copyright (c) Ingeniweb (tm) and
+Contributors. All rights reserved.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the above
+ copyright notice, this list of conditions, and the following
+ disclaimer.
+
+2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+3. The name Ingeniweb (tm) must not be used to
+ endorse or promote products derived from this software
+ without prior written permission from Ingeniweb.
+
+4. The right to distribute this software or to use it for
+ any purpose does not give you the right to use Servicemarks
+ (sm) or Trademarks (tm) of Ingeniweb.
+
+5. If any files are modified, you must cause the modified
+ files to carry prominent notices stating that you changed
+ the files and the date of any change.
+
+Disclaimer
+
+ THIS SOFTWARE IS PROVIDED BY INGENIWEB ``AS IS''
+ AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ NO EVENT SHALL INGENIWEB OR ITS CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
+
+
+This software consists of contributions made by Ingeniweb
+and many individuals on behalf of Ingeniweb.
+Specific attributions are listed in the
+accompanying credits file.
\ No newline at end of file
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+One can override the following variables :
+
+LOG_LEVEL : The log level, from 0 to 5.
+A Log level n implies all logs from 0 to n.
+LOG_LEVEL MUST BE OVERRIDEN !!!!!
+
+
+LOG_NONE = 0 => No log output
+LOG_CRITICAL = 1 => Critical problems (data consistency, module integrity, ...)
+LOG_ERROR = 2 => Error (runtime exceptions, ...)
+LOG_WARNING = 3 => Warning (non-blocking exceptions, ...)
+LOG_NOTICE = 4 => Notices (Special conditions, ...)
+LOG_DEBUG = 5 => Debug (Debugging information)
+
+
+LOG_PROCESSOR : A dictionnary holding, for each key, the data processor.
+A data processor is a function that takes only one parameter : the data to print.
+Default : LogFile for all keys.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: Log.py 33389 2006-11-11 11:24:41Z shh42 $
+__docformat__ = 'restructuredtext'
+
+
+
+LOG_LEVEL = -1
+
+LOG_NONE = 0
+LOG_CRITICAL = 1
+LOG_ERROR = 2
+LOG_WARNING = 3
+LOG_NOTICE = 4
+LOG_DEBUG = 5
+
+from sys import stdout, stderr, exc_info
+import time
+import thread
+import threading
+import traceback
+import os
+import pprint
+import string
+
+LOG_STACK_DEPTH = [-2]
+
+def Log(level, *args):
+ """
+ Log(level, *args) => Pretty-prints data on the console with additional information.
+ """
+ if LOG_LEVEL and level <= LOG_LEVEL:
+ if not level in LOG_PROCESSOR.keys():
+ raise ValueError, "Invalid log level :", level
+
+ stack = ""
+ stackItems = traceback.extract_stack()
+ for depth in LOG_STACK_DEPTH:
+ stackItem = stackItems[depth]
+ stack = "%s%s:%s:" % (stack, os.path.basename(stackItem[0]), stackItem[1],)
+ pr = "%8s %s%s: " % (
+ LOG_LABEL[level],
+ stack,
+ time.ctime(time.time()),
+ )
+ for data in args:
+ try:
+ if "\n" in data:
+ data = data
+ else:
+ data = pprint.pformat(data)
+ except:
+ data = pprint.pformat(data)
+ pr = pr + data + " "
+
+ LOG_PROCESSOR[level](level, LOG_LABEL[level], pr, )
+
+def LogCallStack(level, *args):
+ """
+ LogCallStack(level, *args) => View the whole call stack for the specified call
+ """
+ if LOG_LEVEL and level <= LOG_LEVEL:
+ if not level in LOG_PROCESSOR.keys():
+ raise ValueError, "Invalid log level :", level
+
+ stack = string.join(traceback.format_list(traceback.extract_stack()[:-1]))
+ pr = "%8s %s:\n%s\n" % (
+ LOG_LABEL[level],
+ time.ctime(time.time()),
+ stack
+ )
+ for data in args:
+ try:
+ if "\n" in data:
+ data = data
+ else:
+ data = pprint.pformat(data)
+ except:
+ data = pprint.pformat(data)
+ pr = pr + data + " "
+
+ LOG_PROCESSOR[level](level, LOG_LABEL[level], pr, )
+
+
+
+def FormatStack(stack):
+ """
+ FormatStack(stack) => string
+
+ Return a 'loggable' version of the stack trace
+ """
+ ret = ""
+ for s in stack:
+ ret = ret + "%s:%s:%s: %s\n" % (os.path.basename(s[0]), s[1], s[2], s[3])
+ return ret
+
+
+def LogException():
+ """
+ LogException () => None
+
+ Print an exception information on the console
+ """
+ Log(LOG_NOTICE, "EXCEPTION >>>")
+ traceback.print_exc(file = LOG_OUTPUT)
+ Log(LOG_NOTICE, "<<< EXCEPTION")
+
+
+LOG_OUTPUT = stderr
+def LogFile(level, label, data, ):
+ """
+ LogFile : writes data to the LOG_OUTPUT file.
+ """
+ LOG_OUTPUT.write(data+'\n')
+ LOG_OUTPUT.flush()
+
+
+import logging
+
+CUSTOM_TRACE = 5
+logging.addLevelName('TRACE', CUSTOM_TRACE)
+
+zLogLevelConverter = {
+ LOG_NONE: CUSTOM_TRACE,
+ LOG_CRITICAL: logging.CRITICAL,
+ LOG_ERROR: logging.ERROR,
+ LOG_WARNING: logging.WARNING,
+ LOG_NOTICE: logging.INFO,
+ LOG_DEBUG: logging.DEBUG,
+ }
+
+def LogzLog(level, label, data, ):
+ """
+ LogzLog : writes data though Zope's logging facility
+ """
+ logger = logging.getLogger('GroupUserFolder')
+ logger.log(zLogLevelConverter[level], data + "\n", )
+
+
+
+LOG_PROCESSOR = {
+ LOG_NONE: LogzLog,
+ LOG_CRITICAL: LogzLog,
+ LOG_ERROR: LogzLog,
+ LOG_WARNING: LogzLog,
+ LOG_NOTICE: LogzLog,
+ LOG_DEBUG: LogFile,
+ }
+
+
+LOG_LABEL = {
+ LOG_NONE: "",
+ LOG_CRITICAL: "CRITICAL",
+ LOG_ERROR: "ERROR ",
+ LOG_WARNING: "WARNING ",
+ LOG_NOTICE: "NOTICE ",
+ LOG_DEBUG: "DEBUG ",
+ }
--- /dev/null
+GroupUserFolder
--- /dev/null
+"""
+$Id: PatchCatalogTool.py,v 1.3 2003/07/10 15:27:22 pjgrizel dead $
+"""
+
+try:
+ from Products.CMFCore.CatalogTool import CatalogTool
+except ImportError:
+ pass
+else:
+ if not hasattr(CatalogTool, '_old_listAllowedRolesAndUsers'):
+ def _listAllowedRolesAndUsers(self, user):
+ result = self._old_listAllowedRolesAndUsers(user)
+ getGroups = getattr(user, 'getGroups', None)
+ if getGroups is not None:
+ for group in getGroups():
+ result.append('user:'+group)
+ return result
+
+ from zLOG import LOG, INFO
+ LOG('GroupUserFolder', INFO, 'Patching CatalogTool')
+
+ CatalogTool._old_listAllowedRolesAndUsers = CatalogTool._listAllowedRolesAndUsers
+ CatalogTool._listAllowedRolesAndUsers = _listAllowedRolesAndUsers
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+ GRUF3 Feature-preview stuff.
+
+ This code shouldn't be here but allow people to preview advanced GRUF3
+ features (eg. flexible LDAP searching in 'sharing' tab, ...) in Plone 2,
+ without having to upgrade to Plone 2.1.
+
+ Methods here are monkey-patched by now but will be provided directly by
+ Plone 2.1.
+ Please forgive this 'uglyness' but some users really want to have full
+ LDAP support without switching to the latest Plone version ! ;)
+
+
+ BY DEFAULT, this thing IS enabled with Plone 2.0.x
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: PloneFeaturePreview.py 587 2008-07-31 09:20:06Z pin $
+__docformat__ = 'restructuredtext'
+
+from Products.CMFCore.utils import UniqueObject
+from Products.CMFCore.utils import getToolByName
+from OFS.SimpleItem import SimpleItem
+from OFS.Image import Image
+from Globals import InitializeClass, DTMLFile, MessageDialog
+from Acquisition import aq_base
+from AccessControl.User import nobody
+from AccessControl import ClassSecurityInfo
+from Products.CMFCore.ActionProviderBase import ActionProviderBase
+from interfaces.portal_groups import portal_groups as IGroupsTool
+from global_symbols import *
+
+
+# This is "stollen" from MembershipTool.py
+# this should probably be in MemberDataTool.py
+def searchForMembers( self, REQUEST=None, **kw ):
+ """
+ searchForMembers(self, REQUEST=None, **kw) => normal or fast search method.
+
+ The following properties can be provided:
+ - name
+ - email
+ - last_login_time
+ - roles
+
+ This is an 'AND' request.
+
+ If name is provided, then a _fast_ search is performed with GRUF's
+ searchUsersByName() method. This will improve performance.
+
+ In any other case, a regular (possibly _slow_) search is performed.
+ As it uses the listMembers() method, which is itself based on gruf.getUsers(),
+ this can return partial results. This may change in the future.
+ """
+ md = self.portal_memberdata
+ mt = self.portal_membership
+ if REQUEST:
+ dict = REQUEST
+ else:
+ dict = kw
+
+ # Attributes retreiving & mangling
+ name = dict.get('name', None)
+ email = dict.get('email', None)
+ roles = dict.get('roles', None)
+ last_login_time = dict.get('last_login_time', None)
+ is_manager = mt.checkPermission('Manage portal', self)
+ if name:
+ name = name.strip().lower()
+ if email:
+ email = email.strip().lower()
+
+
+ # We want 'name' request to be handled properly with large user folders.
+ # So we have to check both the fullname and loginname, without scanning all
+ # possible users.
+ md_users = None
+ uf_users = None
+ if name:
+ # We first find in MemberDataTool users whose _full_ name match what we want.
+ lst = md.searchMemberDataContents('fullname', name)
+ md_users = [ x['username'] for x in lst ]
+
+ # Fast search management if the underlying acl_users support it.
+ # This will allow us to retreive users by their _id_ (not name).
+ acl_users = self.acl_users
+ meth = getattr(acl_users, "searchUsersByName", None)
+ if meth:
+ uf_users = meth(name) # gruf search
+
+ # Now we have to merge both lists to get a nice users set.
+ # This is possible only if both lists are filled (or we may miss users else).
+ Log(LOG_DEBUG, md_users, uf_users, )
+ members = []
+ if md_users is not None and uf_users is not None:
+ names_checked = 1
+ wrap = mt.wrapUser
+ getUser = acl_users.getUser
+ for userid in md_users:
+ members.append(wrap(getUser(userid)))
+ for userid in uf_users:
+ if userid in md_users:
+ continue # Kill dupes
+ usr = getUser(userid)
+ if usr is not None:
+ members.append(wrap(usr))
+
+ # Optimization trick
+ if not email and \
+ not roles and \
+ not last_login_time:
+ return members
+ else:
+ # If the lists are not available, we just stupidly get the members list
+ members = self.listMembers()
+ names_checked = 0
+
+ # Now perform individual checks on each user
+ res = []
+ portal = self.portal_url.getPortalObject()
+
+ for member in members:
+ #user = md.wrapUser(u)
+ u = member.getUser()
+ if not (member.listed or is_manager):
+ continue
+ if name and not names_checked:
+ if (u.getUserName().lower().find(name) == -1 and
+ member.getProperty('fullname').lower().find(name) == -1):
+ continue
+ if email:
+ if member.getProperty('email').lower().find(email) == -1:
+ continue
+ if roles:
+ user_roles = member.getRoles()
+ found = 0
+ for r in roles:
+ if r in user_roles:
+ found = 1
+ break
+ if not found:
+ continue
+ if last_login_time:
+ if member.last_login_time < last_login_time:
+ continue
+ res.append(member)
+ Log(LOG_DEBUG, res)
+ return res
+
+
+def listAllowedMembers(self,):
+ """listAllowedMembers => list only members which belong
+ to the same groups/roles as the calling user.
+ """
+ user = self.REQUEST['AUTHENTICATED_USER']
+ caller_roles = user.getRoles() # Have to provide a hook for admins
+ current_members = self.listMembers()
+ allowed_members =[]
+ for member in current_members:
+ for role in caller_roles:
+ if role in member.getRoles():
+ allowed_members.append(member)
+ break
+ return allowed_members
+
+
+def _getPortrait(self, member_id):
+ """
+ return member_id's portrait if you can.
+ If it's not possible, just try to fetch a 'portait' property from the underlying user source,
+ then create a portrait from it.
+ """
+ # fetch the 'portrait' property
+ Log(LOG_DEBUG, "trying to fetch the portrait for the given member id")
+ portrait = self._former_getPortrait(member_id)
+ if portrait:
+ Log(LOG_DEBUG, "Returning the old-style portrait:", portrait, "for", member_id)
+ return portrait
+
+ # Try to find a portrait in the user source
+ member = self.portal_membership.getMemberById(member_id)
+ portrait = member.getUser().getProperty('portrait', None)
+ if not portrait:
+ Log(LOG_DEBUG, "No portrait available in the user source for", member_id)
+ return None
+
+ # Convert the user-source portrait into a plone-complyant one
+ Log(LOG_DEBUG, "Converting the portrait", type(portrait))
+ portrait = Image(id=member_id, file=portrait, title='')
+ membertool = self.portal_memberdata
+ membertool._setPortrait(portrait, member_id)
+
+ # Re-call ourself to retreive the real portrait
+ Log(LOG_DEBUG, "Returning the real portrait")
+ return self._former_getPortrait(member_id)
+
+
+def setLocalRoles( self, obj, member_ids, member_role, reindex=1 ):
+ """ Set local roles on an item """
+ member = self.getAuthenticatedMember()
+ gruf = self.acl_users
+ my_roles = member.getRolesInContext( obj )
+
+ if 'Manager' in my_roles or member_role in my_roles:
+ for member_id in member_ids:
+ u = gruf.getUserById(member_id) or gruf.getGroupByName(member_id)
+ if not u:
+ continue
+ member_id = u.getUserId()
+ roles = list(obj.get_local_roles_for_userid( userid=member_id ))
+
+ if member_role not in roles:
+ roles.append( member_role )
+ obj.manage_setLocalRoles( member_id, roles )
+
+ if reindex:
+ # It is assumed that all objects have the method
+ # reindexObjectSecurity, which is in CMFCatalogAware and
+ # thus PortalContent and PortalFolder.
+ obj.reindexObjectSecurity()
+
+def deleteLocalRoles( self, obj, member_ids, reindex=1 ):
+ """ Delete local roles for members member_ids """
+ member = self.getAuthenticatedMember()
+ my_roles = member.getRolesInContext( obj )
+ gruf = self.acl_users
+ member_ids = [
+ u.getUserId() for u in [
+ gruf.getUserById(u) or gruf.getGroupByName(u) for u in member_ids
+ ] if u
+ ]
+
+ if 'Manager' in my_roles or 'Owner' in my_roles:
+ obj.manage_delLocalRoles( userids=member_ids )
+
+ if reindex:
+ obj.reindexObjectSecurity()
+
+# Monkeypatch it !
+if PREVIEW_PLONE21_IN_PLONE20_:
+ from Products.CMFCore import MembershipTool as CMFCoreMembershipTool
+ CMFCoreMembershipTool.MembershipTool.setLocalRoles = setLocalRoles
+ CMFCoreMembershipTool.MembershipTool.deleteLocalRoles = deleteLocalRoles
+ from Products.CMFPlone import MemberDataTool
+ from Products.CMFPlone import MembershipTool
+ MembershipTool.MembershipTool.searchForMembers = searchForMembers
+ MembershipTool.MembershipTool.listAllowedMembers = listAllowedMembers
+ MemberDataTool.MemberDataTool._former_getPortrait = MemberDataTool.MemberDataTool._getPortrait
+ MemberDataTool.MemberDataTool._getPortrait = _getPortrait
+ Log(LOG_NOTICE, "Applied GRUF's monkeypatch over Plone 2.0.x. Enjoy!")
+
+
+
--- /dev/null
+GroupUserFolder
+
+
+(c)2002-03-04 Ingeniweb
+
+
+
+(This is a structured-text formated file)
+
+
+
+ABSTRACT
+
+ GroupUserFolder is a kind of user folder that provides a special kind of user management.
+ Some users are "flagged" as GROUP and then normal users will be able to belong to one or
+ serveral groups.
+
+ See http://ingeniweb.sourceforge.net/Products/GroupUserFolder for detailed information.
+
+DOWNLOAD
+
+ See http://sourceforge.net/project/showfiles.php?group_id=55262&package_id=81576
+
+
+STRUCTURE
+
+ Group and "normal" User management is distinct. Here's a typical GroupUserFolder hierarchy::
+
+ - acl_users (GroupUserFolder)
+ |
+ |-- Users (GroupUserFolder-related class)
+ | |
+ | |-- acl_users (UserFolder or derived class)
+ |
+ |-- Groups (GroupUserFolder-related class)
+ | |
+ | |-- acl_users (UserFolder or derived class)
+
+
+ So, INSIDE the GroupUserFolder (or GRUF), there are 2 acl_users :
+
+ - The one in the 'Users' object manages real users
+
+ - The one in the 'Groups' object manages groups
+
+ The two acl_users are completely independants. They can even be of different kinds.
+ For example, a Zope UserFolder for Groups management and an LDAPUserFolder for Users management.
+
+ Inside the "Users" acl_users, groups are seen as ROLES (that's what we call "groles") so that
+ roles can be assigned to users using the same storage as regular users. Groups are prefixed
+ by "group " so that they could be easily recognized within roles.
+
+ Then, on the top GroupUserFolder, groups and roles both are seen as users, and users have their
+ normal behaviour (ie. "groles" are not shown), except that users affected to one or several groups
+ have their roles extended with the roles affected to the groups they belong to.
+
+
+ Just for information : one user can belong to zero, one or more groups.
+ One group can have zero, one or more users affected.
+
+ [2003-05-10] There's currently no way to get a list of all users belonging to a particular group.
+
+
+GROUPS BEHAVIOUR
+
+
+ ...will be documented soon...
+
+
+GRUF AND PLONE
+
+ See the dedicated README-Plone file.
+
+
+GRUF AND SimpleUserFolder
+
+ You might think there is a bug using GRUF with SimpleUserFolder (but there's not): if you create
+ a SimpleUserFolder within a GRUF a try to see it from the ZMI, you will get an InfiniteRecursionError.
+
+ That's because SimpleUserFolder tries to fetch a getUserNames() method and finds GRUF's one, which
+ tries to call SimpleUserFolder's one which tries to fetch a getUserNames() method and finds GRUF's one,
+ which tries to call SimpleUserFolder's one which tries to fetch a getUserNames() method and finds GRUF's one,
+ which tries to call SimpleUserFolder's one which tries to fetch a getUserNames() method and finds GRUF's
+ one, which tries to call SimpleUserFolder's one which tries to fetch a getUserNames() method and finds
+ GRUF's one, which tries to call SimpleUserFolder's one which tries to fetch a getUserNames() method and
+ finds GRUF's one, which tries to call SimpleUserFolder's one which tries (see what I mean ?)
+
+ To avoid this, just create a new_getUserNames() object (according to SimpleUserFolder specification) in the folder
+ where you put your SimpleUserFolder in (ie. one of 'Users' or 'Groups' folders).
+
+ GRUF also implies that the SimpleUserFolder methods you create are defined in the 'Users' or 'Groups' folder.
+ If you define them above in the ZODB hierarchy, they will never be acquired and GRUF ones will be catched
+ instead, causing infinite recursions.
+
+
+GRUF AND LDAPUserFolder
+
+ [NEW IN 3.0 VERSION: PLEASE READ README-LDAP.stx INSTEAD]
+
+BUGS
+
+ There is a bug using GRUF with Zope 2.5 and Plone 1.0Beta3 : when trying to join the plone site
+ as a new user, there is a Zope error "Unable to unpickle object"... I don't know how to fix that now.
+ With Zope 2.6 there is no such bug.
+
+DEBUG
+
+ If you put a file named 'debug.txt' in your GRUF's product directory, it will switch the product in
+ debug mode next time you restart Zope. This is the common behaviour for all Ingeniweb products.
+ Debug mode is normally just a way of printing more things on the console. But, with GRUF, debug
+ mode (since 3.1 version) enables a basic user source integrity check. If you've got a broken user
+ folder product on your hard drive that you use as a source with GRUF, it will allow you to unlock
+ the situation.
+
+LICENCE
+
+ GRUF > 2 is released under the terms of the Zope Public Licence (ZPL). Specific arrangements can be found for closed-source projects : please contact us.
+
--- /dev/null
+3.52 - 2006-05-30
+=================
+
+This version has been tested successfuly with the following products.
+It may not depend on all those products but if you experience problems you may track this down.
+
+ * GroupUserFolder 3.52
+
+
+
+3.51 - 2006-05-15
+=================
+
+This version has been tested successfuly with the following products.
+It may not depend on all those products but if you experience problems you may track this down.
+
+ * GroupUserFolder 3.51
+
+
+
+
--- /dev/null
+TODO-LIST
+
+ * Virer lien cliquable onglet "Users" sur utilisateurs qui ne sont PAS dans
+ getUserNames()
+
+ * check caches ?
+
+ * Corriger le bug des arguments par défaut:
+
+ - grep -R "def.*= \[\]" *
+
+ - grep -R "def.*= {}" *
+
+ Cf. http://www.ferg.org/projects/python_gotchas.html#bct_sec_5
+
+[v3.2]
+
+ * Reactivated cache expiration code (thanks to J.P. LADAGE)
+
+ * GRUF3 preview mode with Plone2.0.x
+
+
+[v3.1]
+
+ * Allow LocalRole blacklisting
+
+ * [Plone] Allow user property mutation: needs MembershipTool update !
+
+ * [ZMI] Add an "Add group/roles" and "Remove group/roles" along with the
+ current "Change" button on users/group view (thanks to Danny Bloemendaal)
+
+ * [ZMI] Improve ZMI for large users lists (batching, 'select all' buttons,
+ 'expand all' for tree, ...)
+
+ * [CMF] Test within CMF (not only Plone)
+
+ * [ZMI] Improve users/groups admin screens:
+
+ - use thin borders for audit table and fix cell width
+
+ - add a 'toggle getUserNames()' button on 'Users' tab and use getUsers() by default
+
+[v3.0 => Planned 2003-06]
+
+ DONE * [LDAP] Improve group mapping for already existing groups
+
+ DONE * [Core] Implement join()/leave() methods (and logic!) on groups
+
+ DONE * [Core] Implement some feature to make LDAPUF roles/groups binding easier
+
+ DONE * Plone tools refactoring (user interface must support nested groups, cleaning necessary,
+ API refactoring necessary)
+
+ DONE * FIX DOCUMENTATION (especially README & INSTALLs)
+
+ DONE * Apply security on API methods
+
+ DONE * Check users overview : users disapear sometimes
+
+ DONE * Pass to ZPL licence
+
+ DONE * [CMF/Plone] Test & Document change_password
+
+ DONE * [CMF/Plone] Test & Document searchResults
+
+ DONE * [Doc] Document the whole GRUF API
+
+[v1.4 => Planned 2003-08-31]
+
+ DONE * [Core] Implement multi-UserFolder-sources
+
+ * [Core/ZMI] Implement something to list all members of a particular group
+ and put this view in individual group management screen.
+
+[v1.31 => Planned 2003-08-31]
+
+ DONE * [Core] Fix impossible group removing in users view
+
+ DONE * [ZMI] Optimize screens
+
+ DONE * [CMF/Plone] Fix groups loss when changing pw
+
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: __init__.py 40111 2007-04-01 09:12:57Z alecm $
+__docformat__ = 'restructuredtext'
+
+# postonly protections
+try:
+ # Zope 2.8.9, 2.9.7 and 2.10.3 (and up)
+ from AccessControl.requestmethod import postonly
+except ImportError:
+ try:
+ # Try the hotfix too
+ from Products.Hotfix_20070320 import postonly
+ except:
+ def postonly(callable): return callable
+
+
+import GroupUserFolder
+import GRUFFolder
+import PatchCatalogTool
+try:
+ import Products.LDAPUserFolder
+ hasLDAP = 1
+except ImportError:
+ hasLDAP = 0
+from global_symbols import *
+
+# Plone import try/except
+try:
+ from Products.CMFCore.DirectoryView import registerDirectory
+ import GroupsToolPermissions
+except:
+ # No registerdir available -> we ignore
+ pass
+
+# Used in Extension/install.py
+global groupuserfolder_globals
+groupuserfolder_globals=globals()
+
+# LDAPUserFolder patching
+if hasLDAP:
+ import LDAPGroupFolder
+
+ def patch_LDAPUF():
+ # Now we can patch LDAPUF
+ from Products.LDAPUserFolder import LDAPUserFolder
+ import LDAPUserFolderAdapter
+ LDAPUserFolder._doAddUser = LDAPUserFolderAdapter._doAddUser
+ LDAPUserFolder._doDelUsers = LDAPUserFolderAdapter._doDelUsers
+ LDAPUserFolder._doChangeUser = LDAPUserFolderAdapter._doChangeUser
+ LDAPUserFolder._find_user_dn = LDAPUserFolderAdapter._find_user_dn
+ LDAPUserFolder.manage_editGroupRoles = LDAPUserFolderAdapter.manage_editGroupRoles
+ LDAPUserFolder._mangleRoles = LDAPUserFolderAdapter._mangleRoles
+
+ # Patch LDAPUF : XXX FIXME: have to find something cleaner here?
+ patch_LDAPUF()
+
+def initialize(context):
+
+ try:
+ registerDirectory('skins', groupuserfolder_globals)
+ except:
+ # No registerdir available => we ignore
+ pass
+
+ context.registerClass(
+ GroupUserFolder.GroupUserFolder,
+ permission='Add GroupUserFolders',
+ constructors=(GroupUserFolder.manage_addGroupUserFolder,),
+ icon='www/GroupUserFolder.gif',
+ )
+
+ if hasLDAP:
+ context.registerClass(
+ LDAPGroupFolder.LDAPGroupFolder,
+ permission='Add GroupUserFolders',
+ constructors=(LDAPGroupFolder.addLDAPGroupFolderForm, LDAPGroupFolder.manage_addLDAPGroupFolder,),
+ icon='www/LDAPGroupFolder.gif',
+ )
+
+ context.registerClass(
+ GRUFFolder.GRUFUsers,
+ permission='Add GroupUserFolder',
+ constructors=(GRUFFolder.manage_addGRUFUsers,),
+ visibility=None,
+ icon='www/GRUFUsers.gif',
+ )
+
+ context.registerClass(
+ GRUFFolder.GRUFGroups,
+ permission='Add GroupUserFolder',
+ constructors=(GRUFFolder.manage_addGRUFGroups,),
+ visibility=None,
+ icon='www/GRUFGroups.gif',
+ )
+
+ try:
+ from Products.CMFCore.utils import ToolInit, ContentInit
+ from GroupsTool import GroupsTool
+ from GroupDataTool import GroupDataTool
+ ToolInit( meta_type='CMF Groups Tool'
+ , tools=( GroupsTool, GroupDataTool, )
+ , icon="tool.gif"
+ ).initialize( context )
+
+ except ImportError:
+ Log(LOG_NOTICE, "Unable to import GroupsTool and/or GroupDataTool. \
+ This won't disable GRUF but if you use CMF/Plone you won't get benefit of its special features.")
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: class_utility.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+import string
+import re
+import threading
+import string
+
+# Base classes global vars management
+_BASECLASSESLOCK = threading.RLock()
+_BASECLASSES = {}
+_BASEMETALOCK = threading.RLock()
+_BASEMETA = {}
+
+def showaq(self, indent=''):
+ "showaq"
+ rval = ""
+ obj = self
+ base = getattr(obj, 'aq_base', obj)
+ try: id = base.id
+ except: id = str(base)
+ try: id = id()
+ except: pass
+
+ if hasattr(obj, 'aq_self'):
+ if hasattr(obj.aq_self, 'aq_self'):
+ rval = rval + indent + "(" + id + ")\n"
+ rval = rval + indent + "| \\\n"
+ rval = rval + showaq(obj.aq_self, '| ' + indent)
+ rval = rval + indent + "|\n"
+ if hasattr(obj, 'aq_parent'):
+ rval = rval + indent + id + "\n"
+ rval = rval + indent + "|\n"
+ rval = rval + showaq(obj.aq_parent, indent)
+ else:
+ rval = rval + indent + id + "\n"
+ return rval
+
+
+def listBaseMetaTypes(cl, reverse = 0):
+ """
+ listBaseMetaTypes(cl, reverse = 0) => list of strings
+
+ List all base meta types for this class.
+ """
+ # Look for the class in _BASEMETA cache
+ try:
+ return _BASEMETA[cl][reverse]
+
+ except KeyError:
+ _populateBaseMetaTypes(cl)
+ return listBaseMetaTypes(cl, reverse)
+
+def isBaseMetaType(meta, cl):
+ try:
+ return _BASEMETA[cl][2].has_key(meta)
+
+ except KeyError:
+ _populateBaseMetaTypes(cl)
+ return isBaseMetaType(meta, cl)
+
+def _populateBaseMetaTypes(cl):
+ """Fill the base classes structure"""
+ # Fill the base classes list
+ try:
+ ret = [cl.meta_type]
+ except AttributeError:
+ ret = []
+
+ for b in cl.__bases__:
+ ret = list(listBaseMetaTypes(b, 1)) + ret
+
+ # Fill the base classes dict
+ bases = {}
+ for b in ret:
+ bases[b] = 1
+
+ _BASEMETALOCK.acquire()
+ try:
+ rev = ret[:]
+ rev.reverse()
+ _BASEMETA[cl] = (tuple(rev), tuple(ret), bases)
+ finally:
+ _BASEMETALOCK.release()
+
+def objectIds(container, meta_types = []):
+ """
+ """
+ return map(lambda x: x[0], objectItems(container, meta_types))
+
+def objectValues(container, meta_types = []):
+ """
+ """
+ return map(lambda x: x[1], objectItems(container, meta_types))
+
+def objectItems(container, meta_types = []):
+ """
+ objectItems(container, meta_types = [])
+ Same as a container's objectItem method, meta_types are scanned in the base classes too.
+ Ie. all objects derivated from Folder will be returned by objectItem(x, ['Folder'])
+ """
+ # Convert input type
+ if type(meta_types) not in (type(()), type([])):
+ meta_types = [meta_types]
+
+ # Special case where meta_types is empty
+ if not meta_types:
+ return container.objectItems()
+
+ # Otherwise : check parent for each meta_type
+ ret = []
+ for (id, obj) in container.objectItems():
+ for mt in meta_types:
+ if isBaseMetaType(mt, obj.__class__):
+ ret.append((id, obj))
+ break
+
+ return ret
+
+
+
+def listBaseClasses(cl, reverse = 0):
+ """
+ listBaseClasses(cl, reverse = 0) => list of classes
+
+ List all the base classes of an object.
+ When reverse is 0, return the self class first.
+ When reverse is 1, return the self class last.
+
+ WARNING : reverse is 0 or 1, it is an integer, NOT A BOOLEAN ! (optim issue)
+
+ CACHE RESULTS
+
+ WARNING : for optimization issues, the ORIGINAL tuple is returned : please do not change it !
+ """
+ # Look for the class in _BASECLASSES cache
+ try:
+ return _BASECLASSES[cl][reverse]
+
+ except:
+ _populateBaseClasses(cl)
+ return listBaseClasses(cl, reverse)
+
+
+def isBaseClass(base, cl):
+ """
+ isBaseClass(base, cl) => Boolean
+ Return true if base is a base class of cl
+ """
+ try:
+ return _BASECLASSES[cl][2].has_key(base)
+ except:
+ _populateBaseClasses(cl)
+ return isBaseClass(base, cl)
+
+
+def _populateBaseClasses(cl):
+ """Fill the base classes structure"""
+ # Fill the base classes list
+ ret = [cl]
+ for b in cl.__bases__:
+ ret = list(listBaseClasses(b, 1)) + ret
+
+ # Fill the base classes dict
+ bases = {}
+ for b in ret:
+ bases[b] = 1
+
+ _BASECLASSESLOCK.acquire()
+ try:
+ rev = ret[:]
+ rev.reverse()
+ _BASECLASSES[cl] = (tuple(rev), tuple(ret), bases)
+ finally:
+ _BASECLASSESLOCK.release()
--- /dev/null
+#!/bin/sh
+exec perl -w -x $0 ${1+"$@"} # -*- mode: perl; perl-indent-level: 2; -*-
+#!perl -w
+
+
+##############################################################
+### ###
+### cvs2cl.pl: produce ChangeLog(s) from `cvs log` output. ###
+### ###
+##############################################################
+
+## $Revision: 1.2 $
+## $Date: 2005-08-19 23:51:07 +0200 (ven, 19 aoû 2005) $
+## $Author: dreamcatcher $
+##
+## (C) 2001,2002,2003 Martyn J. Pearce <fluffy@cpan.org>, under the GNU GPL.
+## (C) 1999 Karl Fogel <kfogel@red-bean.com>, under the GNU GPL.
+##
+## (Extensively hacked on by Melissa O'Neill <oneill@cs.sfu.ca>.)
+##
+## cvs2cl.pl 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, or (at your option)
+## any later version.
+##
+## cvs2cl.pl 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 may have received a copy of the GNU General Public License
+## along with cvs2cl.pl; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+## Boston, MA 02111-1307, USA.
+
+\f
+use strict;
+use Text::Wrap;
+use Time::Local;
+use File::Basename;
+
+\f
+# The Plan:
+#
+# Read in the logs for multiple files, spit out a nice ChangeLog that
+# mirrors the information entered during `cvs commit'.
+#
+# The problem presents some challenges. In an ideal world, we could
+# detect files with the same author, log message, and checkin time --
+# each <filelist, author, time, logmessage> would be a changelog entry.
+# We'd sort them; and spit them out. Unfortunately, CVS is *not atomic*
+# so checkins can span a range of times. Also, the directory structure
+# could be hierarchical.
+#
+# Another question is whether we really want to have the ChangeLog
+# exactly reflect commits. An author could issue two related commits,
+# with different log entries, reflecting a single logical change to the
+# source. GNU style ChangeLogs group these under a single author/date.
+# We try to do the same.
+#
+# So, we parse the output of `cvs log', storing log messages in a
+# multilevel hash that stores the mapping:
+# directory => author => time => message => filelist
+# As we go, we notice "nearby" commit times and store them together
+# (i.e., under the same timestamp), so they appear in the same log
+# entry.
+#
+# When we've read all the logs, we twist this mapping into
+# a time => author => message => filelist mapping for each directory.
+#
+# If we're not using the `--distributed' flag, the directory is always
+# considered to be `./', even as descend into subdirectories.
+
+\f
+############### Globals ################
+
+# What we run to generate it:
+my $Log_Source_Command = "cvs log";
+
+# In case we have to print it out:
+my $VERSION = '$Revision: 1.2 $';
+$VERSION =~ s/\S+\s+(\S+)\s+\S+/$1/;
+
+## Vars set by options:
+
+# Print debugging messages?
+my $Debug = 0;
+
+# Just show version and exit?
+my $Print_Version = 0;
+
+# Just print usage message and exit?
+my $Print_Usage = 0;
+
+# Single top-level ChangeLog, or one per subdirectory?
+my $Distributed = 0;
+
+# What file should we generate (defaults to "ChangeLog")?
+my $Log_File_Name = "ChangeLog";
+
+# Grab most recent entry date from existing ChangeLog file, just add
+# to that ChangeLog.
+my $Cumulative = 0;
+
+# Expand usernames to email addresses based on a map file?
+my $User_Map_File = "";
+
+# Output to a file or to stdout?
+my $Output_To_Stdout = 0;
+
+# Eliminate empty log messages?
+my $Prune_Empty_Msgs = 0;
+
+# Tags of which not to output
+my @ignore_tags;
+
+# Don't call Text::Wrap on the body of the message
+my $No_Wrap = 0;
+
+# Separates header from log message. Code assumes it is either " " or
+# "\n\n", so if there's ever an option to set it to something else,
+# make sure to go through all conditionals that use this var.
+my $After_Header = " ";
+
+# XML Encoding
+my $XML_Encoding = '';
+
+# Format more for programs than for humans.
+my $XML_Output = 0;
+
+# Do some special tweaks for log data that was written in FSF
+# ChangeLog style.
+my $FSF_Style = 0;
+
+# Show times in UTC instead of local time
+my $UTC_Times = 0;
+
+# Show times in output?
+my $Show_Times = 1;
+
+# Show day of week in output?
+my $Show_Day_Of_Week = 0;
+
+# Show revision numbers in output?
+my $Show_Revisions = 0;
+
+# Show tags (symbolic names) in output?
+my $Show_Tags = 0;
+
+# Show tags separately in output?
+my $Show_Tag_Dates = 0;
+
+# Show branches by symbolic name in output?
+my $Show_Branches = 0;
+
+# Show only revisions on these branches or their ancestors.
+my @Follow_Branches;
+
+# Don't bother with files matching this regexp.
+my @Ignore_Files;
+
+# How exactly we match entries. We definitely want "o",
+# and user might add "i" by using --case-insensitive option.
+my $Case_Insensitive = 0;
+
+# Maybe only show log messages matching a certain regular expression.
+my $Regexp_Gate = "";
+
+# Pass this global option string along to cvs, to the left of `log':
+my $Global_Opts = "";
+
+# Pass this option string along to the cvs log subcommand:
+my $Command_Opts = "";
+
+# Read log output from stdin instead of invoking cvs log?
+my $Input_From_Stdin = 0;
+
+# Don't show filenames in output.
+my $Hide_Filenames = 0;
+
+# Max checkin duration. CVS checkin is not atomic, so we may have checkin
+# times that span a range of time. We assume that checkins will last no
+# longer than $Max_Checkin_Duration seconds, and that similarly, no
+# checkins will happen from the same users with the same message less
+# than $Max_Checkin_Duration seconds apart.
+my $Max_Checkin_Duration = 180;
+
+# What to put at the front of [each] ChangeLog.
+my $ChangeLog_Header = "";
+
+# Whether to enable 'delta' mode, and for what start/end tags.
+my $Delta_Mode = 0;
+my $Delta_From = "";
+my $Delta_To = "";
+
+## end vars set by options.
+
+# latest observed times for the start/end tags in delta mode
+my $Delta_StartTime = 0;
+my $Delta_EndTime = 0;
+
+# In 'cvs log' output, one long unbroken line of equal signs separates
+# files:
+my $file_separator = "======================================="
+ . "======================================";
+
+# In 'cvs log' output, a shorter line of dashes separates log messages
+# within a file:
+my $logmsg_separator = "----------------------------";
+
+############### End globals ############
+
+\f
+&parse_options ();
+&derive_change_log ();
+
+\f
+### Everything below is subroutine definitions. ###
+
+# If accumulating, grab the boundary date from pre-existing ChangeLog.
+sub maybe_grab_accumulation_date ()
+{
+ if (! $Cumulative) {
+ return "";
+ }
+
+ # else
+
+ open (LOG, "$Log_File_Name")
+ or die ("trouble opening $Log_File_Name for reading ($!)");
+
+ my $boundary_date;
+ while (<LOG>)
+ {
+ if (/^(\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)/)
+ {
+ $boundary_date = "$1";
+ last;
+ }
+ }
+
+ close (LOG);
+ return $boundary_date;
+}
+
+# Fills up a ChangeLog structure in the current directory.
+sub derive_change_log ()
+{
+ # See "The Plan" above for a full explanation.
+
+ my %grand_poobah;
+
+ my $file_full_path;
+ my $time;
+ my $revision;
+ my $author;
+ my $msg_txt;
+ my $detected_file_separator;
+
+ my %tag_date_printed;
+
+ # Might be adding to an existing ChangeLog
+ my $accumulation_date = &maybe_grab_accumulation_date ();
+ if ($accumulation_date) {
+ # Insert -d immediately after 'cvs log'
+ my $Log_Date_Command = "-d\'>${accumulation_date}\'";
+ $Log_Source_Command =~ s/(^.*log\S*)/$1 $Log_Date_Command/;
+ &debug ("(adding log msg starting from $accumulation_date)\n");
+ }
+
+ # We might be expanding usernames
+ my %usermap;
+
+ # In general, it's probably not very maintainable to use state
+ # variables like this to tell the loop what it's doing at any given
+ # moment, but this is only the first one, and if we never have more
+ # than a few of these, it's okay.
+ my $collecting_symbolic_names = 0;
+ my %symbolic_names; # Where tag names get stored.
+ my %branch_names; # We'll grab branch names while we're at it.
+ my %branch_numbers; # Save some revisions for @Follow_Branches
+ my @branch_roots; # For showing which files are branch ancestors.
+
+ # Bleargh. Compensate for a deficiency of custom wrapping.
+ if (($After_Header ne " ") and $FSF_Style)
+ {
+ $After_Header .= "\t";
+ }
+
+ if (! $Input_From_Stdin) {
+ &debug ("(run \"${Log_Source_Command}\")\n");
+ open (LOG_SOURCE, "$Log_Source_Command |")
+ or die "unable to run \"${Log_Source_Command}\"";
+ }
+ else {
+ open (LOG_SOURCE, "-") or die "unable to open stdin for reading";
+ }
+
+ binmode LOG_SOURCE;
+
+ %usermap = &maybe_read_user_map_file ();
+
+ while (<LOG_SOURCE>)
+ {
+ # Canonicalize line endings
+ s/\r$//;
+ # If on a new file and don't see filename, skip until we find it, and
+ # when we find it, grab it.
+ if ((! (defined $file_full_path)) and /^Working file: (.*)/)
+ {
+ $file_full_path = $1;
+ if (@Ignore_Files)
+ {
+ my $base;
+ ($base, undef, undef) = fileparse ($file_full_path);
+ # Ouch, I wish trailing operators in regexps could be
+ # evaluated on the fly!
+ if ($Case_Insensitive) {
+ if (grep ($file_full_path =~ m|$_|i, @Ignore_Files)) {
+ undef $file_full_path;
+ }
+ }
+ elsif (grep ($file_full_path =~ m|$_|, @Ignore_Files)) {
+ undef $file_full_path;
+ }
+ }
+ next;
+ }
+
+ # Just spin wheels if no file defined yet.
+ next if (! $file_full_path);
+
+ # Collect tag names in case we're asked to print them in the output.
+ if (/^symbolic names:$/) {
+ $collecting_symbolic_names = 1;
+ next; # There's no more info on this line, so skip to next
+ }
+ if ($collecting_symbolic_names)
+ {
+ # All tag names are listed with whitespace in front in cvs log
+ # output; so if see non-whitespace, then we're done collecting.
+ if (/^\S/) {
+ $collecting_symbolic_names = 0;
+ }
+ else # we're looking at a tag name, so parse & store it
+ {
+ # According to the Cederqvist manual, in node "Tags", tag
+ # names must start with an uppercase or lowercase letter and
+ # can contain uppercase and lowercase letters, digits, `-',
+ # and `_'. However, it's not our place to enforce that, so
+ # we'll allow anything CVS hands us to be a tag:
+ /^\s+([^:]+): ([\d.]+)$/;
+ my $tag_name = $1;
+ my $tag_rev = $2;
+
+ # A branch number either has an odd number of digit sections
+ # (and hence an even number of dots), or has ".0." as the
+ # second-to-last digit section. Test for these conditions.
+ my $real_branch_rev = "";
+ if (($tag_rev =~ /^(\d+\.\d+\.)+\d+$/) # Even number of dots...
+ and (! ($tag_rev =~ /^(1\.)+1$/))) # ...but not "1.[1.]1"
+ {
+ $real_branch_rev = $tag_rev;
+ }
+ elsif ($tag_rev =~ /(\d+\.(\d+\.)+)0.(\d+)/) # Has ".0."
+ {
+ $real_branch_rev = $1 . $3;
+ }
+ # If we got a branch, record its number.
+ if ($real_branch_rev)
+ {
+ $branch_names{$real_branch_rev} = $tag_name;
+ if (@Follow_Branches) {
+ if (grep ($_ eq $tag_name, @Follow_Branches)) {
+ $branch_numbers{$tag_name} = $real_branch_rev;
+ }
+ }
+ }
+ else {
+ # Else it's just a regular (non-branch) tag.
+ push (@{$symbolic_names{$tag_rev}}, $tag_name);
+ }
+ }
+ }
+ # End of code for collecting tag names.
+
+ # If have file name, but not revision, and see revision, then grab
+ # it. (We collect unconditionally, even though we may or may not
+ # ever use it.)
+ if ((! (defined $revision)) and (/^revision (\d+\.[\d.]+)/))
+ {
+ $revision = $1;
+
+ if (@Follow_Branches)
+ {
+ foreach my $branch (@Follow_Branches)
+ {
+ # Special case for following trunk revisions
+ if (($branch =~ /^trunk$/i) and ($revision =~ /^[0-9]+\.[0-9]+$/))
+ {
+ goto dengo;
+ }
+
+ my $branch_number = $branch_numbers{$branch};
+ if ($branch_number)
+ {
+ # Are we on one of the follow branches or an ancestor of
+ # same?
+ #
+ # If this revision is a prefix of the branch number, or
+ # possibly is less in the minormost number, OR if this
+ # branch number is a prefix of the revision, then yes.
+ # Otherwise, no.
+ #
+ # So below, we determine if any of those conditions are
+ # met.
+
+ # Trivial case: is this revision on the branch?
+ # (Compare this way to avoid regexps that screw up Emacs
+ # indentation, argh.)
+ if ((substr ($revision, 0, ((length ($branch_number)) + 1)))
+ eq ($branch_number . "."))
+ {
+ goto dengo;
+ }
+ # Non-trivial case: check if rev is ancestral to branch
+ elsif ((length ($branch_number)) > (length ($revision)))
+ {
+ $revision =~ /^((?:\d+\.)+)(\d+)$/;
+ my $r_left = $1; # still has the trailing "."
+ my $r_end = $2;
+
+ $branch_number =~ /^((?:\d+\.)+)(\d+)\.\d+$/;
+ my $b_left = $1; # still has trailing "."
+ my $b_mid = $2; # has no trailing "."
+
+ if (($r_left eq $b_left)
+ && ($r_end <= $b_mid))
+ {
+ goto dengo;
+ }
+ }
+ }
+ }
+ }
+ else # (! @Follow_Branches)
+ {
+ next;
+ }
+
+ # Else we are following branches, but this revision isn't on the
+ # path. So skip it.
+ undef $revision;
+ dengo:
+ next;
+ }
+
+ # If we don't have a revision right now, we couldn't possibly
+ # be looking at anything useful.
+ if (! (defined ($revision))) {
+ $detected_file_separator = /^$file_separator$/o;
+ if ($detected_file_separator) {
+ # No revisions for this file; can happen, e.g. "cvs log -d DATE"
+ goto CLEAR;
+ }
+ else {
+ next;
+ }
+ }
+
+ # If have file name but not date and author, and see date or
+ # author, then grab them:
+ unless (defined $time)
+ {
+ if (/^date: .*/)
+ {
+ ($time, $author) = &parse_date_and_author ($_);
+ if (defined ($usermap{$author}) and $usermap{$author}) {
+ $author = $usermap{$author};
+ }
+ }
+ else {
+ $detected_file_separator = /^$file_separator$/o;
+ if ($detected_file_separator) {
+ # No revisions for this file; can happen, e.g. "cvs log -d DATE"
+ goto CLEAR;
+ }
+ }
+ # If the date/time/author hasn't been found yet, we couldn't
+ # possibly care about anything we see. So skip:
+ next;
+ }
+
+ # A "branches: ..." line here indicates that one or more branches
+ # are rooted at this revision. If we're showing branches, then we
+ # want to show that fact as well, so we collect all the branches
+ # that this is the latest ancestor of and store them in
+ # @branch_roots. Just for reference, the format of the line we're
+ # seeing at this point is:
+ #
+ # branches: 1.5.2; 1.5.4; ...;
+ #
+ # Okay, here goes:
+
+ if (/^branches:\s+(.*);$/)
+ {
+ if ($Show_Branches)
+ {
+ my $lst = $1;
+ $lst =~ s/(1\.)+1;|(1\.)+1$//; # ignore the trivial branch 1.1.1
+ if ($lst) {
+ @branch_roots = split (/;\s+/, $lst);
+ }
+ else {
+ undef @branch_roots;
+ }
+ next;
+ }
+ else
+ {
+ # Ugh. This really bothers me. Suppose we see a log entry
+ # like this:
+ #
+ # ----------------------------
+ # revision 1.1
+ # date: 1999/10/17 03:07:38; author: jrandom; state: Exp;
+ # branches: 1.1.2;
+ # Intended first line of log message begins here.
+ # ----------------------------
+ #
+ # The question is, how we can tell the difference between that
+ # log message and a *two*-line log message whose first line is
+ #
+ # "branches: 1.1.2;"
+ #
+ # See the problem? The output of "cvs log" is inherently
+ # ambiguous.
+ #
+ # For now, we punt: we liberally assume that people don't
+ # write log messages like that, and just toss a "branches:"
+ # line if we see it but are not showing branches. I hope no
+ # one ever loses real log data because of this.
+ next;
+ }
+ }
+
+ # If have file name, time, and author, then we're just grabbing
+ # log message texts:
+ $detected_file_separator = /^$file_separator$/o;
+ if ($detected_file_separator && ! (defined $revision)) {
+ # No revisions for this file; can happen, e.g. "cvs log -d DATE"
+ goto CLEAR;
+ }
+ unless ($detected_file_separator || /^$logmsg_separator$/o)
+ {
+ $msg_txt .= $_; # Normally, just accumulate the message...
+ next;
+ }
+ # ... until a msg separator is encountered:
+ # Ensure the message contains something:
+ if ((! $msg_txt)
+ || ($msg_txt =~ /^\s*\.\s*$|^\s*$/)
+ || ($msg_txt =~ /\*\*\* empty log message \*\*\*/))
+ {
+ if ($Prune_Empty_Msgs) {
+ goto CLEAR;
+ }
+ # else
+ $msg_txt = "[no log message]\n";
+ }
+
+ ### Store it all in the Grand Poobah:
+ {
+ my $dir_key; # key into %grand_poobah
+ my %qunk; # complicated little jobbie, see below
+
+ # Each revision of a file has a little data structure (a `qunk')
+ # associated with it. That data structure holds not only the
+ # file's name, but any additional information about the file
+ # that might be needed in the output, such as the revision
+ # number, tags, branches, etc. The reason to have these things
+ # arranged in a data structure, instead of just appending them
+ # textually to the file's name, is that we may want to do a
+ # little rearranging later as we write the output. For example,
+ # all the files on a given tag/branch will go together, followed
+ # by the tag in parentheses (so trunk or otherwise non-tagged
+ # files would go at the end of the file list for a given log
+ # message). This rearrangement is a lot easier to do if we
+ # don't have to reparse the text.
+ #
+ # A qunk looks like this:
+ #
+ # {
+ # filename => "hello.c",
+ # revision => "1.4.3.2",
+ # time => a timegm() return value (moment of commit)
+ # tags => [ "tag1", "tag2", ... ],
+ # branch => "branchname" # There should be only one, right?
+ # branchroots => [ "branchtag1", "branchtag2", ... ]
+ # }
+
+ if ($Distributed) {
+ # Just the basename, don't include the path.
+ ($qunk{'filename'}, $dir_key, undef) = fileparse ($file_full_path);
+ }
+ else {
+ $dir_key = "./";
+ $qunk{'filename'} = $file_full_path;
+ }
+
+ # This may someday be used in a more sophisticated calculation
+ # of what other files are involved in this commit. For now, we
+ # don't use it much except for delta mode, because the
+ # common-commit-detection algorithm is hypothesized to be
+ # "good enough" as it stands.
+ $qunk{'time'} = $time;
+
+ # We might be including revision numbers and/or tags and/or
+ # branch names in the output. Most of the code from here to
+ # loop-end deals with organizing these in qunk.
+
+ $qunk{'revision'} = $revision;
+
+ # Grab the branch, even though we may or may not need it:
+ $qunk{'revision'} =~ /((?:\d+\.)+)\d+/;
+ my $branch_prefix = $1;
+ $branch_prefix =~ s/\.$//; # strip off final dot
+ if ($branch_names{$branch_prefix}) {
+ $qunk{'branch'} = $branch_names{$branch_prefix};
+ }
+
+ # If there's anything in the @branch_roots array, then this
+ # revision is the root of at least one branch. We'll display
+ # them as branch names instead of revision numbers, the
+ # substitution for which is done directly in the array:
+ if (@branch_roots) {
+ my @roots = map { $branch_names{$_} } @branch_roots;
+ $qunk{'branchroots'} = \@roots;
+ }
+
+ # Save tags too.
+ if (defined ($symbolic_names{$revision})) {
+ $qunk{'tags'} = $symbolic_names{$revision};
+ delete $symbolic_names{$revision};
+
+ # If we're in 'delta' mode, update the latest observed
+ # times for the beginning and ending tags, and
+ # when we get around to printing output, we will simply restrict
+ # ourselves to that timeframe...
+
+ if ($Delta_Mode) {
+ if (($time > $Delta_StartTime) &&
+ (grep { $_ eq $Delta_From } @{$qunk{'tags'}}))
+ {
+ $Delta_StartTime = $time;
+ }
+
+ if (($time > $Delta_EndTime) &&
+ (grep { $_ eq $Delta_To } @{$qunk{'tags'}}))
+ {
+ $Delta_EndTime = $time;
+ }
+ }
+ }
+
+ # Add this file to the list
+ # (We use many spoonfuls of autovivication magic. Hashes and arrays
+ # will spring into existence if they aren't there already.)
+
+ &debug ("(pushing log msg for ${dir_key}$qunk{'filename'})\n");
+
+ # Store with the files in this commit. Later we'll loop through
+ # again, making sure that revisions with the same log message
+ # and nearby commit times are grouped together as one commit.
+ push (@{$grand_poobah{$dir_key}{$author}{$time}{$msg_txt}}, \%qunk);
+ }
+
+ CLEAR:
+ # Make way for the next message
+ undef $msg_txt;
+ undef $time;
+ undef $revision;
+ undef $author;
+ undef @branch_roots;
+
+ # Maybe even make way for the next file:
+ if ($detected_file_separator) {
+ undef $file_full_path;
+ undef %branch_names;
+ undef %branch_numbers;
+ undef %symbolic_names;
+ }
+ }
+
+ close (LOG_SOURCE);
+
+ ### Process each ChangeLog
+
+ while (my ($dir,$authorhash) = each %grand_poobah)
+ {
+ &debug ("DOING DIR: $dir\n");
+
+ # Here we twist our hash around, from being
+ # author => time => message => filelist
+ # in %$authorhash to
+ # time => author => message => filelist
+ # in %changelog.
+ #
+ # This is also where we merge entries. The algorithm proceeds
+ # through the timeline of the changelog with a sliding window of
+ # $Max_Checkin_Duration seconds; within that window, entries that
+ # have the same log message are merged.
+ #
+ # (To save space, we zap %$authorhash after we've copied
+ # everything out of it.)
+
+ my %changelog;
+ while (my ($author,$timehash) = each %$authorhash)
+ {
+ my $lasttime;
+ my %stamptime;
+ foreach my $time (sort {$main::a <=> $main::b} (keys %$timehash))
+ {
+ my $msghash = $timehash->{$time};
+ while (my ($msg,$qunklist) = each %$msghash)
+ {
+ my $stamptime = $stamptime{$msg};
+ if ((defined $stamptime)
+ and (($time - $stamptime) < $Max_Checkin_Duration)
+ and (defined $changelog{$stamptime}{$author}{$msg}))
+ {
+ push(@{$changelog{$stamptime}{$author}{$msg}}, @$qunklist);
+ }
+ else {
+ $changelog{$time}{$author}{$msg} = $qunklist;
+ $stamptime{$msg} = $time;
+ }
+ }
+ }
+ }
+ undef (%$authorhash);
+
+ ### Now we can write out the ChangeLog!
+
+ my ($logfile_here, $logfile_bak, $tmpfile);
+
+ if (! $Output_To_Stdout) {
+ $logfile_here = $dir . $Log_File_Name;
+ $logfile_here =~ s/^\.\/\//\//; # fix any leading ".//" problem
+ $tmpfile = "${logfile_here}.cvs2cl$$.tmp";
+ $logfile_bak = "${logfile_here}.bak";
+
+ open (LOG_OUT, ">$tmpfile") or die "Unable to open \"$tmpfile\"";
+ }
+ else {
+ open (LOG_OUT, ">-") or die "Unable to open stdout for writing";
+ }
+
+ print LOG_OUT $ChangeLog_Header;
+
+ if ($XML_Output) {
+ my $encoding =
+ length $XML_Encoding ? qq'encoding="$XML_Encoding"' : '';
+ my $version = 'version="1.0"';
+ my $declaration =
+ sprintf '<?xml %s?>', join ' ', grep length, $version, $encoding;
+ my $root =
+ '<changelog xmlns="http://www.red-bean.com/xmlns/cvs2cl/">';
+ print LOG_OUT "$declaration\n\n$root\n\n";
+ }
+
+ foreach my $time (sort {$main::b <=> $main::a} (keys %changelog))
+ {
+ next if ($Delta_Mode &&
+ (($time <= $Delta_StartTime) ||
+ ($time > $Delta_EndTime && $Delta_EndTime)));
+
+ # Set up the date/author line.
+ # kff todo: do some more XML munging here, on the header
+ # part of the entry:
+ my ($ignore,$min,$hour,$mday,$mon,$year,$wday)
+ = $UTC_Times ? gmtime($time) : localtime($time);
+
+ # XML output includes everything else, we might as well make
+ # it always include Day Of Week too, for consistency.
+ if ($Show_Day_Of_Week or $XML_Output) {
+ $wday = ("Sunday", "Monday", "Tuesday", "Wednesday",
+ "Thursday", "Friday", "Saturday")[$wday];
+ $wday = ($XML_Output) ? "<weekday>${wday}</weekday>\n" : " $wday";
+ }
+ else {
+ $wday = "";
+ }
+
+ my $authorhash = $changelog{$time};
+ if ($Show_Tag_Dates) {
+ my %tags;
+ while (my ($author,$mesghash) = each %$authorhash) {
+ while (my ($msg,$qunk) = each %$mesghash) {
+ foreach my $qunkref2 (@$qunk) {
+ if (defined ($$qunkref2{'tags'})) {
+ foreach my $tag (@{$$qunkref2{'tags'}}) {
+ $tags{$tag} = 1;
+ }
+ }
+ }
+ }
+ }
+ foreach my $tag (keys %tags) {
+ if (!defined $tag_date_printed{$tag}) {
+ $tag_date_printed{$tag} = $time;
+ if ($XML_Output) {
+ # NOT YET DONE
+ }
+ else {
+ if ($Show_Times) {
+ printf LOG_OUT ("%4u-%02u-%02u${wday} %02u:%02u tag %s\n\n",
+ $year+1900, $mon+1, $mday, $hour, $min, $tag);
+ } else {
+ printf LOG_OUT ("%4u-%02u-%02u${wday} tag %s\n\n",
+ $year+1900, $mon+1, $mday, $tag);
+ }
+ }
+ }
+ }
+ }
+ while (my ($author,$mesghash) = each %$authorhash)
+ {
+ # If XML, escape in outer loop to avoid compound quoting:
+ if ($XML_Output) {
+ $author = &xml_escape ($author);
+ }
+
+ FOOBIE:
+ while (my ($msg,$qunklist) = each %$mesghash)
+ {
+ ## MJP: 19.xii.01 : Exclude @ignore_tags
+ for my $ignore_tag (@ignore_tags) {
+ next FOOBIE
+ if grep $_ eq $ignore_tag, map(@{$_->{tags}},
+ grep(defined $_->{tags},
+ @$qunklist));
+ }
+ ## MJP: 19.xii.01 : End exclude @ignore_tags
+
+ my $files = &pretty_file_list ($qunklist);
+ my $header_line; # date and author
+ my $body; # see below
+ my $wholething; # $header_line + $body
+
+ if ($XML_Output) {
+ $header_line =
+ sprintf ("<date>%4u-%02u-%02u</date>\n"
+ . "${wday}"
+ . "<time>%02u:%02u</time>\n"
+ . "<author>%s</author>\n",
+ $year+1900, $mon+1, $mday, $hour, $min, $author);
+ }
+ else {
+ if ($Show_Times) {
+ $header_line =
+ sprintf ("%4u-%02u-%02u${wday} %02u:%02u %s\n\n",
+ $year+1900, $mon+1, $mday, $hour, $min, $author);
+ } else {
+ $header_line =
+ sprintf ("%4u-%02u-%02u${wday} %s\n\n",
+ $year+1900, $mon+1, $mday, $author);
+ }
+ }
+
+ $Text::Wrap::huge = 'overflow'
+ if $Text::Wrap::VERSION >= 2001.0130;
+ # Reshape the body according to user preferences.
+ if ($XML_Output)
+ {
+ $msg = &preprocess_msg_text ($msg);
+ $body = $files . $msg;
+ }
+ elsif ($No_Wrap)
+ {
+ $msg = &preprocess_msg_text ($msg);
+ $files = wrap ("\t", " ", "$files");
+ $msg =~ s/\n(.*)/\n\t$1/g;
+ unless ($After_Header eq " ") {
+ $msg =~ s/^(.*)/\t$1/g;
+ }
+ $body = $files . $After_Header . $msg;
+ }
+ else # do wrapping, either FSF-style or regular
+ {
+ if ($FSF_Style)
+ {
+ $files = wrap ("\t", " ", "$files");
+
+ my $files_last_line_len = 0;
+ if ($After_Header eq " ")
+ {
+ $files_last_line_len = &last_line_len ($files);
+ $files_last_line_len += 1; # for $After_Header
+ }
+
+ $msg = &wrap_log_entry
+ ($msg, "\t", 69 - $files_last_line_len, 69);
+ $body = $files . $After_Header . $msg;
+ }
+ else # not FSF-style
+ {
+ $msg = &preprocess_msg_text ($msg);
+ $body = $files . $After_Header . $msg;
+ $body = wrap ("\t", " ", "$body");
+ }
+ }
+
+ $wholething = $header_line . $body;
+
+ if ($XML_Output) {
+ $wholething = "<entry>\n${wholething}</entry>\n";
+ }
+
+ # One last check: make sure it passes the regexp test, if the
+ # user asked for that. We have to do it here, so that the
+ # test can match against information in the header as well
+ # as in the text of the log message.
+
+ # How annoying to duplicate so much code just because I
+ # can't figure out a way to evaluate scalars on the trailing
+ # operator portion of a regular expression. Grrr.
+ if ($Case_Insensitive) {
+ unless ($Regexp_Gate && ($wholething !~ /$Regexp_Gate/oi)) {
+ print LOG_OUT "${wholething}\n";
+ }
+ }
+ else {
+ unless ($Regexp_Gate && ($wholething !~ /$Regexp_Gate/o)) {
+ print LOG_OUT "${wholething}\n";
+ }
+ }
+ }
+ }
+ }
+
+ if ($XML_Output) {
+ print LOG_OUT "</changelog>\n";
+ }
+
+ close (LOG_OUT);
+
+ if (! $Output_To_Stdout)
+ {
+ # If accumulating, append old data to new before renaming. But
+ # don't append the most recent entry, since it's already in the
+ # new log due to CVS's idiosyncratic interpretation of "log -d".
+ if ($Cumulative && -f $logfile_here)
+ {
+ open (NEW_LOG, ">>$tmpfile")
+ or die "trouble appending to $tmpfile ($!)";
+
+ open (OLD_LOG, "<$logfile_here")
+ or die "trouble reading from $logfile_here ($!)";
+
+ my $started_first_entry = 0;
+ my $passed_first_entry = 0;
+ while (<OLD_LOG>)
+ {
+ if (! $passed_first_entry)
+ {
+ if ((! $started_first_entry)
+ && /^(\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)/) {
+ $started_first_entry = 1;
+ }
+ elsif (/^(\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)/) {
+ $passed_first_entry = 1;
+ print NEW_LOG $_;
+ }
+ }
+ else {
+ print NEW_LOG $_;
+ }
+ }
+
+ close (NEW_LOG);
+ close (OLD_LOG);
+ }
+
+ if (-f $logfile_here) {
+ rename ($logfile_here, $logfile_bak);
+ }
+ rename ($tmpfile, $logfile_here);
+ }
+ }
+}
+
+sub parse_date_and_author ()
+{
+ # Parses the date/time and author out of a line like:
+ #
+ # date: 1999/02/19 23:29:05; author: apharris; state: Exp;
+
+ my $line = shift;
+
+ my ($year, $mon, $mday, $hours, $min, $secs, $author) = $line =~
+ m#(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);\s+author:\s+([^;]+);#
+ or die "Couldn't parse date ``$line''";
+ die "Bad date or Y2K issues" unless ($year > 1969 and $year < 2258);
+ # Kinda arbitrary, but useful as a sanity check
+ my $time = timegm($secs,$min,$hours,$mday,$mon-1,$year-1900);
+
+ return ($time, $author);
+}
+
+# Here we take a bunch of qunks and convert them into printed
+# summary that will include all the information the user asked for.
+sub pretty_file_list ()
+{
+ if ($Hide_Filenames and (! $XML_Output)) {
+ return "";
+ }
+
+ my $qunksref = shift;
+ my @qunkrefs = @$qunksref;
+ my @filenames;
+ my $beauty = ""; # The accumulating header string for this entry.
+ my %non_unanimous_tags; # Tags found in a proper subset of qunks
+ my %unanimous_tags; # Tags found in all qunks
+ my %all_branches; # Branches found in any qunk
+ my $common_dir = undef; # Dir prefix common to all files ("" if none)
+ my $fbegun = 0; # Did we begin printing filenames yet?
+
+ # First, loop over the qunks gathering all the tag/branch names.
+ # We'll put them all in non_unanimous_tags, and take out the
+ # unanimous ones later.
+ QUNKREF:
+ foreach my $qunkref (@qunkrefs)
+ {
+ ## MJP: 19.xii.01 : Exclude @ignore_tags
+ for my $ignore_tag (@ignore_tags) {
+ next QUNKREF
+ if grep $_ eq $ignore_tag, @{$$qunkref{'tags'}};
+ }
+ ## MJP: 19.xii.01 : End exclude @ignore_tags
+
+ # Keep track of whether all the files in this commit were in the
+ # same directory, and memorize it if so. We can make the output a
+ # little more compact by mentioning the directory only once.
+ if ((scalar (@qunkrefs)) > 1)
+ {
+ if (! (defined ($common_dir)))
+ {
+ my ($base, $dir);
+ ($base, $dir, undef) = fileparse ($$qunkref{'filename'});
+
+ if ((! (defined ($dir))) # this first case is sheer paranoia
+ or ($dir eq "")
+ or ($dir eq "./")
+ or ($dir eq ".\\"))
+ {
+ $common_dir = "";
+ }
+ else
+ {
+ $common_dir = $dir;
+ }
+ }
+ elsif ($common_dir ne "")
+ {
+ # Already have a common dir prefix, so how much of it can we preserve?
+ $common_dir = &common_path_prefix ($$qunkref{'filename'}, $common_dir);
+ }
+ }
+ else # only one file in this entry anyway, so common dir not an issue
+ {
+ $common_dir = "";
+ }
+
+ if (defined ($$qunkref{'branch'})) {
+ $all_branches{$$qunkref{'branch'}} = 1;
+ }
+ if (defined ($$qunkref{'tags'})) {
+ foreach my $tag (@{$$qunkref{'tags'}}) {
+ $non_unanimous_tags{$tag} = 1;
+ }
+ }
+ }
+
+ # Any tag held by all qunks will be printed specially... but only if
+ # there are multiple qunks in the first place!
+ if ((scalar (@qunkrefs)) > 1) {
+ foreach my $tag (keys (%non_unanimous_tags)) {
+ my $everyone_has_this_tag = 1;
+ foreach my $qunkref (@qunkrefs) {
+ if ((! (defined ($$qunkref{'tags'})))
+ or (! (grep ($_ eq $tag, @{$$qunkref{'tags'}})))) {
+ $everyone_has_this_tag = 0;
+ }
+ }
+ if ($everyone_has_this_tag) {
+ $unanimous_tags{$tag} = 1;
+ delete $non_unanimous_tags{$tag};
+ }
+ }
+ }
+
+ if ($XML_Output)
+ {
+ # If outputting XML, then our task is pretty simple, because we
+ # don't have to detect common dir, common tags, branch prefixing,
+ # etc. We just output exactly what we have, and don't worry about
+ # redundancy or readability.
+
+ foreach my $qunkref (@qunkrefs)
+ {
+ my $filename = $$qunkref{'filename'};
+ my $revision = $$qunkref{'revision'};
+ my $tags = $$qunkref{'tags'};
+ my $branch = $$qunkref{'branch'};
+ my $branchroots = $$qunkref{'branchroots'};
+
+ $filename = &xml_escape ($filename); # probably paranoia
+ $revision = &xml_escape ($revision); # definitely paranoia
+
+ $beauty .= "<file>\n";
+ $beauty .= "<name>${filename}</name>\n";
+ $beauty .= "<revision>${revision}</revision>\n";
+ if ($branch) {
+ $branch = &xml_escape ($branch); # more paranoia
+ $beauty .= "<branch>${branch}</branch>\n";
+ }
+ foreach my $tag (@$tags) {
+ $tag = &xml_escape ($tag); # by now you're used to the paranoia
+ $beauty .= "<tag>${tag}</tag>\n";
+ }
+ foreach my $root (@$branchroots) {
+ $root = &xml_escape ($root); # which is good, because it will continue
+ $beauty .= "<branchroot>${root}</branchroot>\n";
+ }
+ $beauty .= "</file>\n";
+ }
+
+ # Theoretically, we could go home now. But as long as we're here,
+ # let's print out the common_dir and utags, as a convenience to
+ # the receiver (after all, earlier code calculated that stuff
+ # anyway, so we might as well take advantage of it).
+
+ if ((scalar (keys (%unanimous_tags))) > 1) {
+ foreach my $utag ((keys (%unanimous_tags))) {
+ $utag = &xml_escape ($utag); # the usual paranoia
+ $beauty .= "<utag>${utag}</utag>\n";
+ }
+ }
+ if ($common_dir) {
+ $common_dir = &xml_escape ($common_dir);
+ $beauty .= "<commondir>${common_dir}</commondir>\n";
+ }
+
+ # That's enough for XML, time to go home:
+ return $beauty;
+ }
+
+ # Else not XML output, so complexly compactify for chordate
+ # consumption. At this point we have enough global information
+ # about all the qunks to organize them non-redundantly for output.
+
+ if ($common_dir) {
+ # Note that $common_dir still has its trailing slash
+ $beauty .= "$common_dir: ";
+ }
+
+ if ($Show_Branches)
+ {
+ # For trailing revision numbers.
+ my @brevisions;
+
+ foreach my $branch (keys (%all_branches))
+ {
+ foreach my $qunkref (@qunkrefs)
+ {
+ if ((defined ($$qunkref{'branch'}))
+ and ($$qunkref{'branch'} eq $branch))
+ {
+ if ($fbegun) {
+ # kff todo: comma-delimited in XML too? Sure.
+ $beauty .= ", ";
+ }
+ else {
+ $fbegun = 1;
+ }
+ my $fname = substr ($$qunkref{'filename'}, length ($common_dir));
+ $beauty .= $fname;
+ $$qunkref{'printed'} = 1; # Just setting a mark bit, basically
+
+ if ($Show_Tags && (defined @{$$qunkref{'tags'}})) {
+ my @tags = grep ($non_unanimous_tags{$_}, @{$$qunkref{'tags'}});
+
+ if (@tags) {
+ $beauty .= " (tags: ";
+ $beauty .= join (', ', @tags);
+ $beauty .= ")";
+ }
+ }
+
+ if ($Show_Revisions) {
+ # Collect the revision numbers' last components, but don't
+ # print them -- they'll get printed with the branch name
+ # later.
+ $$qunkref{'revision'} =~ /.+\.([\d]+)$/;
+ push (@brevisions, $1);
+
+ # todo: we're still collecting branch roots, but we're not
+ # showing them anywhere. If we do show them, it would be
+ # nifty to just call them revision "0" on a the branch.
+ # Yeah, that's the ticket.
+ }
+ }
+ }
+ $beauty .= " ($branch";
+ if (@brevisions) {
+ if ((scalar (@brevisions)) > 1) {
+ $beauty .= ".[";
+ $beauty .= (join (',', @brevisions));
+ $beauty .= "]";
+ }
+ else {
+ # Square brackets are spurious here, since there's no range to
+ # encapsulate
+ $beauty .= ".$brevisions[0]";
+ }
+ }
+ $beauty .= ")";
+ }
+ }
+
+ # Okay; any qunks that were done according to branch are taken care
+ # of, and marked as printed. Now print everyone else.
+
+ foreach my $qunkref (@qunkrefs)
+ {
+ next if (defined ($$qunkref{'printed'})); # skip if already printed
+
+ if ($fbegun) {
+ $beauty .= ", ";
+ }
+ else {
+ $fbegun = 1;
+ }
+ $beauty .= substr ($$qunkref{'filename'}, length ($common_dir));
+ # todo: Shlomo's change was this:
+ # $beauty .= substr ($$qunkref{'filename'},
+ # (($common_dir eq "./") ? "" : length ($common_dir)));
+ $$qunkref{'printed'} = 1; # Set a mark bit.
+
+ if ($Show_Revisions || $Show_Tags)
+ {
+ my $started_addendum = 0;
+
+ if ($Show_Revisions) {
+ $started_addendum = 1;
+ $beauty .= " (";
+ $beauty .= "$$qunkref{'revision'}";
+ }
+ if ($Show_Tags && (defined $$qunkref{'tags'})) {
+ my @tags = grep ($non_unanimous_tags{$_}, @{$$qunkref{'tags'}});
+ if ((scalar (@tags)) > 0) {
+ if ($started_addendum) {
+ $beauty .= ", ";
+ }
+ else {
+ $beauty .= " (tags: ";
+ }
+ $beauty .= join (', ', @tags);
+ $started_addendum = 1;
+ }
+ }
+ if ($started_addendum) {
+ $beauty .= ")";
+ }
+ }
+ }
+
+ # Unanimous tags always come last.
+ if ($Show_Tags && %unanimous_tags)
+ {
+ $beauty .= " (utags: ";
+ $beauty .= join (', ', sort keys (%unanimous_tags));
+ $beauty .= ")";
+ }
+
+ # todo: still have to take care of branch_roots?
+
+ $beauty = "* $beauty:";
+
+ return $beauty;
+}
+
+sub common_path_prefix ()
+{
+ my $path1 = shift;
+ my $path2 = shift;
+
+ my ($dir1, $dir2);
+ (undef, $dir1, undef) = fileparse ($path1);
+ (undef, $dir2, undef) = fileparse ($path2);
+
+ # Transmogrify Windows filenames to look like Unix.
+ # (It is far more likely that someone is running cvs2cl.pl under
+ # Windows than that they would genuinely have backslashes in their
+ # filenames.)
+ $dir1 =~ tr#\\#/#;
+ $dir2 =~ tr#\\#/#;
+
+ my $accum1 = "";
+ my $accum2 = "";
+ my $last_common_prefix = "";
+
+ while ($accum1 eq $accum2)
+ {
+ $last_common_prefix = $accum1;
+ last if ($accum1 eq $dir1);
+ my ($tmp1) = split (/\//, (substr ($dir1, length ($accum1))));
+ my ($tmp2) = split (/\//, (substr ($dir2, length ($accum2))));
+ $accum1 .= "$tmp1/" if (defined $tmp1 and $tmp1 ne '');
+ $accum2 .= "$tmp2/" if (defined $tmp2 and $tmp2 ne '');
+ }
+
+ return $last_common_prefix;
+}
+
+sub preprocess_msg_text ()
+{
+ my $text = shift;
+
+ # Strip out carriage returns (as they probably result from DOSsy editors).
+ $text =~ s/\r\n/\n/g;
+
+ # If it *looks* like two newlines, make it *be* two newlines:
+ $text =~ s/\n\s*\n/\n\n/g;
+
+ if ($XML_Output)
+ {
+ $text = &xml_escape ($text);
+ $text = "<msg>${text}</msg>\n";
+ }
+ elsif (! $No_Wrap)
+ {
+ # Strip off lone newlines, but only for lines that don't begin with
+ # whitespace or a mail-quoting character, since we want to preserve
+ # that kind of formatting. Also don't strip newlines that follow a
+ # period; we handle those specially next. And don't strip
+ # newlines that precede an open paren.
+ 1 while ($text =~ s/(^|\n)([^>\s].*[^.\n])\n([^>\n])/$1$2 $3/g);
+
+ # If a newline follows a period, make sure that when we bring up the
+ # bottom sentence, it begins with two spaces.
+ 1 while ($text =~ s/(^|\n)([^>\s].*)\n([^>\n])/$1$2 $3/g);
+ }
+
+ return $text;
+}
+
+sub last_line_len ()
+{
+ my $files_list = shift;
+ my @lines = split (/\n/, $files_list);
+ my $last_line = pop (@lines);
+ return length ($last_line);
+}
+
+# A custom wrap function, sensitive to some common constructs used in
+# log entries.
+sub wrap_log_entry ()
+{
+ my $text = shift; # The text to wrap.
+ my $left_pad_str = shift; # String to pad with on the left.
+
+ # These do NOT take left_pad_str into account:
+ my $length_remaining = shift; # Amount left on current line.
+ my $max_line_length = shift; # Amount left for a blank line.
+
+ my $wrapped_text = ""; # The accumulating wrapped entry.
+ my $user_indent = ""; # Inherited user_indent from prev line.
+
+ my $first_time = 1; # First iteration of the loop?
+ my $suppress_line_start_match = 0; # Set to disable line start checks.
+
+ my @lines = split (/\n/, $text);
+ while (@lines) # Don't use `foreach' here, it won't work.
+ {
+ my $this_line = shift (@lines);
+ chomp $this_line;
+
+ if ($this_line =~ /^(\s+)/) {
+ $user_indent = $1;
+ }
+ else {
+ $user_indent = "";
+ }
+
+ # If it matches any of the line-start regexps, print a newline now...
+ if ($suppress_line_start_match)
+ {
+ $suppress_line_start_match = 0;
+ }
+ elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/)
+ || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/)
+ || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/)
+ || ($this_line =~ /^(\s+)(\S+)/)
+ || ($this_line =~ /^(\s*)- +/)
+ || ($this_line =~ /^()\s*$/)
+ || ($this_line =~ /^(\s*)\*\) +/)
+ || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/))
+ {
+ # Make a line break immediately, unless header separator is set
+ # and this line is the first line in the entry, in which case
+ # we're getting the blank line for free already and shouldn't
+ # add an extra one.
+ unless (($After_Header ne " ") and ($first_time))
+ {
+ if ($this_line =~ /^()\s*$/) {
+ $suppress_line_start_match = 1;
+ $wrapped_text .= "\n${left_pad_str}";
+ }
+
+ $wrapped_text .= "\n${left_pad_str}";
+ }
+
+ $length_remaining = $max_line_length - (length ($user_indent));
+ }
+
+ # Now that any user_indent has been preserved, strip off leading
+ # whitespace, so up-folding has no ugly side-effects.
+ $this_line =~ s/^\s*//;
+
+ # Accumulate the line, and adjust parameters for next line.
+ my $this_len = length ($this_line);
+ if ($this_len == 0)
+ {
+ # Blank lines should cancel any user_indent level.
+ $user_indent = "";
+ $length_remaining = $max_line_length;
+ }
+ elsif ($this_len >= $length_remaining) # Line too long, try breaking it.
+ {
+ # Walk backwards from the end. At first acceptable spot, break
+ # a new line.
+ my $idx = $length_remaining - 1;
+ if ($idx < 0) { $idx = 0 };
+ while ($idx > 0)
+ {
+ if (substr ($this_line, $idx, 1) =~ /\s/)
+ {
+ my $line_now = substr ($this_line, 0, $idx);
+ my $next_line = substr ($this_line, $idx);
+ $this_line = $line_now;
+
+ # Clean whitespace off the end.
+ chomp $this_line;
+
+ # The current line is ready to be printed.
+ $this_line .= "\n${left_pad_str}";
+
+ # Make sure the next line is allowed full room.
+ $length_remaining = $max_line_length - (length ($user_indent));
+
+ # Strip next_line, but then preserve any user_indent.
+ $next_line =~ s/^\s*//;
+
+ # Sneak a peek at the user_indent of the upcoming line, so
+ # $next_line (which will now precede it) can inherit that
+ # indent level. Otherwise, use whatever user_indent level
+ # we currently have, which might be none.
+ my $next_next_line = shift (@lines);
+ if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) {
+ $next_line = $1 . $next_line if (defined ($1));
+ # $length_remaining = $max_line_length - (length ($1));
+ $next_next_line =~ s/^\s*//;
+ }
+ else {
+ $next_line = $user_indent . $next_line;
+ }
+ if (defined ($next_next_line)) {
+ unshift (@lines, $next_next_line);
+ }
+ unshift (@lines, $next_line);
+
+ # Our new next line might, coincidentally, begin with one of
+ # the line-start regexps, so we temporarily turn off
+ # sensitivity to that until we're past the line.
+ $suppress_line_start_match = 1;
+
+ last;
+ }
+ else
+ {
+ $idx--;
+ }
+ }
+
+ if ($idx == 0)
+ {
+ # We bottomed out because the line is longer than the
+ # available space. But that could be because the space is
+ # small, or because the line is longer than even the maximum
+ # possible space. Handle both cases below.
+
+ if ($length_remaining == ($max_line_length - (length ($user_indent))))
+ {
+ # The line is simply too long -- there is no hope of ever
+ # breaking it nicely, so just insert it verbatim, with
+ # appropriate padding.
+ $this_line = "\n${left_pad_str}${this_line}";
+ }
+ else
+ {
+ # Can't break it here, but may be able to on the next round...
+ unshift (@lines, $this_line);
+ $length_remaining = $max_line_length - (length ($user_indent));
+ $this_line = "\n${left_pad_str}";
+ }
+ }
+ }
+ else # $this_len < $length_remaining, so tack on what we can.
+ {
+ # Leave a note for the next iteration.
+ $length_remaining = $length_remaining - $this_len;
+
+ if ($this_line =~ /\.$/)
+ {
+ $this_line .= " ";
+ $length_remaining -= 2;
+ }
+ else # not a sentence end
+ {
+ $this_line .= " ";
+ $length_remaining -= 1;
+ }
+ }
+
+ # Unconditionally indicate that loop has run at least once.
+ $first_time = 0;
+
+ $wrapped_text .= "${user_indent}${this_line}";
+ }
+
+ # One last bit of padding.
+ $wrapped_text .= "\n";
+
+ return $wrapped_text;
+}
+
+sub xml_escape ()
+{
+ my $txt = shift;
+ $txt =~ s/&/&/g;
+ $txt =~ s/</</g;
+ $txt =~ s/>/>/g;
+ return $txt;
+}
+
+sub maybe_read_user_map_file ()
+{
+ my %expansions;
+
+ if ($User_Map_File)
+ {
+ open (MAPFILE, "<$User_Map_File")
+ or die ("Unable to open $User_Map_File ($!)");
+
+ while (<MAPFILE>)
+ {
+ next if /^\s*#/; # Skip comment lines.
+ next if not /:/; # Skip lines without colons.
+
+ # It is now safe to split on ':'.
+ my ($username, $expansion) = split ':';
+ chomp $expansion;
+ $expansion =~ s/^'(.*)'$/$1/;
+ $expansion =~ s/^"(.*)"$/$1/;
+
+ # If it looks like the expansion has a real name already, then
+ # we toss the username we got from CVS log. Otherwise, keep
+ # it to use in combination with the email address.
+
+ if ($expansion =~ /^\s*<{0,1}\S+@.*/) {
+ # Also, add angle brackets if none present
+ if (! ($expansion =~ /<\S+@\S+>/)) {
+ $expansions{$username} = "$username <$expansion>";
+ }
+ else {
+ $expansions{$username} = "$username $expansion";
+ }
+ }
+ else {
+ $expansions{$username} = $expansion;
+ }
+ }
+
+ close (MAPFILE);
+ }
+
+ return %expansions;
+}
+
+sub parse_options ()
+{
+ # Check this internally before setting the global variable.
+ my $output_file;
+
+ # If this gets set, we encountered unknown options and will exit at
+ # the end of this subroutine.
+ my $exit_with_admonishment = 0;
+
+ while (my $arg = shift (@ARGV))
+ {
+ if ($arg =~ /^-h$|^-help$|^--help$|^--usage$|^-?$/) {
+ $Print_Usage = 1;
+ }
+ elsif ($arg =~ /^--delta$/) {
+ my $narg = shift(@ARGV) || die "$arg needs argument.\n";
+ if ($narg =~ /^([A-Za-z][A-Za-z0-9_\-]*):([A-Za-z][A-Za-z0-9_\-]*)$/) {
+ $Delta_From = $1;
+ $Delta_To = $2;
+ $Delta_Mode = 1;
+ } else {
+ die "--delta FROM_TAG:TO_TAG is what you meant to say.\n";
+ }
+ }
+ elsif ($arg =~ /^--debug$/) { # unadvertised option, heh
+ $Debug = 1;
+ }
+ elsif ($arg =~ /^--version$/) {
+ $Print_Version = 1;
+ }
+ elsif ($arg =~ /^-g$|^--global-opts$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ # Don't assume CVS is called "cvs" on the user's system:
+ $Log_Source_Command =~ s/(^\S*)/$1 $narg/;
+ }
+ elsif ($arg =~ /^-l$|^--log-opts$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ $Log_Source_Command .= " $narg";
+ }
+ elsif ($arg =~ /^-f$|^--file$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ $output_file = $narg;
+ }
+ elsif ($arg =~ /^--accum$/) {
+ $Cumulative = 1;
+ }
+ elsif ($arg =~ /^--fsf$/) {
+ $FSF_Style = 1;
+ }
+ elsif ($arg =~ /^-U$|^--usermap$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ $User_Map_File = $narg;
+ }
+ elsif ($arg =~ /^-W$|^--window$/) {
+ defined(my $narg = shift (@ARGV)) || die "$arg needs argument.\n";
+ $Max_Checkin_Duration = $narg;
+ }
+ elsif ($arg =~ /^-I$|^--ignore$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ push (@Ignore_Files, $narg);
+ }
+ elsif ($arg =~ /^-C$|^--case-insensitive$/) {
+ $Case_Insensitive = 1;
+ }
+ elsif ($arg =~ /^-R$|^--regexp$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ $Regexp_Gate = $narg;
+ }
+ elsif ($arg =~ /^--stdout$/) {
+ $Output_To_Stdout = 1;
+ }
+ elsif ($arg =~ /^--version$/) {
+ $Print_Version = 1;
+ }
+ elsif ($arg =~ /^-d$|^--distributed$/) {
+ $Distributed = 1;
+ }
+ elsif ($arg =~ /^-P$|^--prune$/) {
+ $Prune_Empty_Msgs = 1;
+ }
+ elsif ($arg =~ /^-S$|^--separate-header$/) {
+ $After_Header = "\n\n";
+ }
+ elsif ($arg =~ /^--no-wrap$/) {
+ $No_Wrap = 1;
+ }
+ elsif ($arg =~ /^--gmt$|^--utc$/) {
+ $UTC_Times = 1;
+ }
+ elsif ($arg =~ /^-w$|^--day-of-week$/) {
+ $Show_Day_Of_Week = 1;
+ }
+ elsif ($arg =~ /^--no-times$/) {
+ $Show_Times = 0;
+ }
+ elsif ($arg =~ /^-r$|^--revisions$/) {
+ $Show_Revisions = 1;
+ }
+ elsif ($arg =~ /^-t$|^--tags$/) {
+ $Show_Tags = 1;
+ }
+ elsif ($arg =~ /^-T$|^--tagdates$/) {
+ $Show_Tag_Dates = 1;
+ }
+ elsif ($arg =~ /^-b$|^--branches$/) {
+ $Show_Branches = 1;
+ }
+ elsif ($arg =~ /^-F$|^--follow$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ push (@Follow_Branches, $narg);
+ }
+ elsif ($arg =~ /^--stdin$/) {
+ $Input_From_Stdin = 1;
+ }
+ elsif ($arg =~ /^--header$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ $ChangeLog_Header = &slurp_file ($narg);
+ if (! defined ($ChangeLog_Header)) {
+ $ChangeLog_Header = "";
+ }
+ }
+ elsif ($arg =~ /^--xml-encoding$/) {
+ my $narg = shift (@ARGV) || die "$arg needs argument.\n";
+ $XML_Encoding = $narg ;
+ }
+ elsif ($arg =~ /^--xml$/) {
+ $XML_Output = 1;
+ }
+ elsif ($arg =~ /^--hide-filenames$/) {
+ $Hide_Filenames = 1;
+ $After_Header = "";
+ }
+ elsif ($arg =~ /^--ignore-tag$/ ) {
+ die "$arg needs argument.\n"
+ unless @ARGV;
+ push @ignore_tags, shift @ARGV;
+ }
+ else {
+ # Just add a filename as argument to the log command
+ $Log_Source_Command .= " '$arg'";
+ }
+ }
+
+ ## Check for contradictions...
+
+ if ($Output_To_Stdout && $Distributed) {
+ print STDERR "cannot pass both --stdout and --distributed\n";
+ $exit_with_admonishment = 1;
+ }
+
+ if ($Output_To_Stdout && $output_file) {
+ print STDERR "cannot pass both --stdout and --file\n";
+ $exit_with_admonishment = 1;
+ }
+
+ if ($XML_Output && $Cumulative) {
+ print STDERR "cannot pass both --xml and --accum\n";
+ $exit_with_admonishment = 1;
+ }
+
+ # Or if any other error message has already been printed out, we
+ # just leave now:
+ if ($exit_with_admonishment) {
+ &usage ();
+ exit (1);
+ }
+ elsif ($Print_Usage) {
+ &usage ();
+ exit (0);
+ }
+ elsif ($Print_Version) {
+ &version ();
+ exit (0);
+ }
+
+ ## Else no problems, so proceed.
+
+ if ($output_file) {
+ $Log_File_Name = $output_file;
+ }
+}
+
+sub slurp_file ()
+{
+ my $filename = shift || die ("no filename passed to slurp_file()");
+ my $retstr;
+
+ open (SLURPEE, "<${filename}") or die ("unable to open $filename ($!)");
+ my $saved_sep = $/;
+ undef $/;
+ $retstr = <SLURPEE>;
+ $/ = $saved_sep;
+ close (SLURPEE);
+ return $retstr;
+}
+
+sub debug ()
+{
+ if ($Debug) {
+ my $msg = shift;
+ print STDERR $msg;
+ }
+}
+
+sub version ()
+{
+ print "cvs2cl.pl version ${VERSION}; distributed under the GNU GPL.\n";
+}
+
+sub usage ()
+{
+ &version ();
+ print <<'END_OF_INFO';
+Generate GNU-style ChangeLogs in CVS working copies.
+
+Notes about the output format(s):
+
+ The default output of cvs2cl.pl is designed to be compact, formally
+ unambiguous, but still easy for humans to read. It is largely
+ self-explanatory, I hope; the one abbreviation that might not be
+ obvious is "utags". That stands for "universal tags" -- a
+ universal tag is one held by all the files in a given change entry.
+
+ If you need output that's easy for a program to parse, use the
+ --xml option. Note that with XML output, just about all available
+ information is included with each change entry, whether you asked
+ for it or not, on the theory that your parser can ignore anything
+ it's not looking for.
+
+Notes about the options and arguments (the actual options are listed
+last in this usage message):
+
+ * The -I and -F options may appear multiple times.
+
+ * To follow trunk revisions, use "-F trunk" ("-F TRUNK" also works).
+ This is okay because no would ever, ever be crazy enough to name a
+ branch "trunk", right? Right.
+
+ * For the -U option, the UFILE should be formatted like
+ CVSROOT/users. That is, each line of UFILE looks like this
+ jrandom:jrandom@red-bean.com
+ or maybe even like this
+ jrandom:'Jesse Q. Random <jrandom@red-bean.com>'
+ Don't forget to quote the portion after the colon if necessary.
+
+ * Many people want to filter by date. To do so, invoke cvs2cl.pl
+ like this:
+ cvs2cl.pl -l "-d'DATESPEC'"
+ where DATESPEC is any date specification valid for "cvs log -d".
+ (Note that CVS 1.10.7 and below requires there be no space between
+ -d and its argument).
+
+Options/Arguments:
+
+ -h, -help, --help, or -? Show this usage and exit
+ --version Show version and exit
+ -r, --revisions Show revision numbers in output
+ -b, --branches Show branch names in revisions when possible
+ -t, --tags Show tags (symbolic names) in output
+ -T, --tagdates Show tags in output on their first occurance
+ --stdin Read from stdin, don't run cvs log
+ --stdout Output to stdout not to ChangeLog
+ -d, --distributed Put ChangeLogs in subdirs
+ -f FILE, --file FILE Write to FILE instead of "ChangeLog"
+ --fsf Use this if log data is in FSF ChangeLog style
+ -W SECS, --window SECS Window of time within which log entries unify
+ -U UFILE, --usermap UFILE Expand usernames to email addresses from UFILE
+ -R REGEXP, --regexp REGEXP Include only entries that match REGEXP
+ -I REGEXP, --ignore REGEXP Ignore files whose names match REGEXP
+ -C, --case-insensitive Any regexp matching is done case-insensitively
+ -F BRANCH, --follow BRANCH Show only revisions on or ancestral to BRANCH
+ -S, --separate-header Blank line between each header and log message
+ --no-wrap Don't auto-wrap log message (recommend -S also)
+ --gmt, --utc Show times in GMT/UTC instead of local time
+ --accum Add to an existing ChangeLog (incompat w/ --xml)
+ -w, --day-of-week Show day of week
+ --no-times Don't show times in output
+ --header FILE Get ChangeLog header from FILE ("-" means stdin)
+ --xml Output XML instead of ChangeLog format
+ --xml-encoding ENCODING Insert encoding clause in XML header
+ --hide-filenames Don't show filenames (ignored for XML output)
+ -P, --prune Don't show empty log messages
+ -g OPTS, --global-opts OPTS Invoke like this "cvs OPTS log ..."
+ -l OPTS, --log-opts OPTS Invoke like this "cvs ... log OPTS"
+ FILE1 [FILE2 ...] Show only log information for the named FILE(s)
+
+See http://www.red-bean.com/cvs2cl for maintenance and bug info.
+END_OF_INFO
+}
+
+__END__
+
+=head1 NAME
+
+cvs2cl.pl - produces GNU-style ChangeLogs in CVS working copies, by
+ running "cvs log" and parsing the output. Shared log entries are
+ unified in an intuitive way.
+
+=head1 DESCRIPTION
+
+This script generates GNU-style ChangeLog files from CVS log
+information. Basic usage: just run it inside a working copy and a
+ChangeLog will appear. It requires repository access (i.e., 'cvs log'
+must work). Run "cvs2cl.pl --help" to see more advanced options.
+
+See http://www.red-bean.com/cvs2cl for updates, and for instructions
+on getting anonymous CVS access to this script.
+
+Maintainer: Karl Fogel <kfogel@red-bean.com>
+Please report bugs to <bug-cvs2cl@red-bean.com>.
+
+=head1 README
+
+This script generates GNU-style ChangeLog files from CVS log
+information. Basic usage: just run it inside a working copy and a
+ChangeLog will appear. It requires repository access (i.e., 'cvs log'
+must work). Run "cvs2cl.pl --help" to see more advanced options.
+
+See http://www.red-bean.com/cvs2cl for updates, and for instructions
+on getting anonymous CVS access to this script.
+
+Maintainer: Karl Fogel <kfogel@red-bean.com>
+Please report bugs to <bug-cvs2cl@red-bean.com>.
+
+=head1 PREREQUISITES
+
+This script requires C<Text::Wrap>, C<Time::Local>, and
+C<File::Basename>.
+It also seems to require C<Perl 5.004_04> or higher.
+
+=pod OSNAMES
+
+any
+
+=pod SCRIPT CATEGORIES
+
+Version_Control/CVS
+
+=cut
+
+-*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*-
+
+Note about a bug-slash-opportunity:
+-----------------------------------
+
+There's a bug in Text::Wrap, which affects cvs2cl. This script
+reveals it:
+
+ #!/usr/bin/perl -w
+
+ use Text::Wrap;
+
+ my $test_text =
+ "This script demonstrates a bug in Text::Wrap. The very long line
+ following this paragraph will be relocated relative to the surrounding
+ text:
+
+ ====================================================================
+
+ See? When the bug happens, we'll get the line of equal signs below
+ this paragraph, even though it should be above.";
+
+ # Print out the test text with no wrapping:
+ print "$test_text";
+ print "\n";
+ print "\n";
+
+ # Now print it out wrapped, and see the bug:
+ print wrap ("\t", " ", "$test_text");
+ print "\n";
+ print "\n";
+
+If the line of equal signs were one shorter, then the bug doesn't
+happen. Interesting.
+
+Anyway, rather than fix this in Text::Wrap, we might as well write a
+new wrap() which has the following much-needed features:
+
+* initial indentation, like current Text::Wrap()
+* subsequent line indentation, like current Text::Wrap()
+* user chooses among: force-break long words, leave them alone, or die()?
+* preserve existing indentation: chopped chunks from an indented line
+ are indented by same (like this line, not counting the asterisk!)
+* optional list of things to preserve on line starts, default ">"
+
+Note that the last two are essentially the same concept, so unify in
+implementation and give a good interface to controlling them.
+
+And how about:
+
+Optionally, when encounter a line pre-indented by same as previous
+line, then strip the newline and refill, but indent by the same.
+Yeah...
+
--- /dev/null
+zope-groupuserfolder (0.3-1) unstable; urgency=low
+
+ * Initial Release.
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Wed, 16 Apr 2003 10:04:50 +0200
+
--- /dev/null
+#!/bin/sh -e
+#----------------------------------------------------------------
+# Simple `.config' script for zope-* packages.
+# First coded by Luca - De Whiskey's - De Vitis <luca@debian.org>
+#----------------------------------------------------------------
+
+# Load the confmodule.
+. /usr/share/debconf/confmodule
+
+# Setup.
+db_version 2.0
+db_capb backup
+
+# Prompt the question to the user.
+db_input low "$(basename $0 .config)/postinst" || true
+db_go
+
+# Stop the communication with the db.
+db_stop
+
+# That's all folks!
+exit 0
--- /dev/null
+Source: zope-groupuserfolder
+Section: web
+Priority: optional
+Maintainer: Sylvain Thenault <sylvain.thenault@logilab.fr>
+Build-Depends: debhelper (>= 3.0.0)
+Standards-Version: 3.5.8
+
+Package: zope-groupuserfolder
+Architecture: all
+Depends: zope
+Description: Group management for Zope [dummy package]
+ GroupUserFolder is a kind of user folder that provides a special kind of user
+ management.
+ Some users are "flagged" as GROUP and then normal users will be able to belong
+ to one or
+ serveral groups.
+ .
+ .
+ This package is an empty dummy package that always depends on
+ a package built for Debian's default Python version.
+
+
--- /dev/null
+This package was debianized by Sylvain Thenault <sylvain.thenault@logilab.fr> Sat, 13 Apr 2002 19:05:23 +0200.
+
+It was downloaded from ftp://ftp.sourceforge.net/pub/sourceforge/collective
+
+Upstream Author:
+
+ P.-J. Grizel <grizel@ingeniweb.com>
+
+Copyright:
+
+Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+Copyright (c) 2002 Ingeniweb SARL
+
+
+This software is distributed under the term of the Zope Public License version 2.0.
+Please, refer to /usr/share/doc/zope/ZPL-2.0
+
+
--- /dev/null
+#! /bin/sh
+#----------------------------------------------------------------
+# Simple `.postinst' script for zope-* packages.
+# First coded by Luca - De Whiskey's - De Vitis <luca@debian.org>
+#----------------------------------------------------------------
+
+set -e
+
+# summary of how this script can be called:
+# * <postinst> `configure' <most-recently-configured-version>
+# * <old-postinst> `abort-upgrade' <new version>
+# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+# <new-version>
+# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+# <failed-install-package> <version> `removing'
+# <conflicting-package> <version>
+# for details, see /usr/doc/packaging-manual/
+#
+# quoting from the policy:
+# Any necessary prompting should almost always be confined to the
+# post-installation script, and should be protected with a conditional
+# so that unnecessary prompting doesn't happen if a package's
+# installation fails and the `postinst' is called with `abort-upgrade',
+# `abort-remove' or `abort-deconfigure'.
+
+# Load confmodule.
+. /usr/share/debconf/confmodule
+db_version 2.0
+
+case "$1" in
+ configure)
+ # Get the answer.
+ db_get "$(basename $0 .postinst)/postinst" || true
+ test "$RET" = "true" && /etc/init.d/zope restart
+ ;;
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 0
+ ;;
+esac
+
+# Stop the communication with the db.
+db_stop
+
+#DEBHELPER#
+
+# That's all folks!
+exit 0
--- /dev/null
+#! /bin/sh
+#----------------------------------------------------------------
+# Simple `.prerm' script for zope-* packages.
+# First coded by Luca - De Whiskey's - De Vitis <luca@debian.org>
+#----------------------------------------------------------------
+
+set -e
+
+# summary of how this script can be called:
+# * <prerm> `remove'
+# * <old-prerm> `upgrade' <new-version>
+# * <new-prerm> `failed-upgrade' <old-version>
+# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
+# * <deconfigured's-prerm> `deconfigure' `in-favour'
+# <package-being-installed> <version> `removing'
+# <conflicting-package> <version>
+# for details, see /usr/share/doc/packaging-manual/
+
+# I simply replaced the PACKAGE variable with the subscript
+dpkg --listfiles $(basename $0 .prerm) |
+ awk '$0~/\.py$/ {print $0"c\n" $0"o"}' |
+ xargs rm -f >&2
+
+case "$1" in
+ remove|upgrade|deconfigure)
+ ;;
+ failed-upgrade)
+ ;;
+ *)
+ echo "prerm called with unknown argument \`$1'" >&2
+ exit 0
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
--- /dev/null
+#!/usr/bin/make -f
+# Sample debian/rules that uses debhelper.
+# GNU copyright 1997 to 1999 by Joey Hess.
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+# This is the debhelper compatability version to use.
+export DH_COMPAT=4
+
+
+
+build: DH_OPTIONS=
+build: build-stamp
+build-stamp:
+ dh_testdir
+
+ touch build-stamp
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp configure-stamp
+ rm -rf build
+ rm -rf debian/python?.?-tmp*
+ dh_clean
+
+install: DH_OPTIONS=
+install: build
+ dh_testdir
+ dh_testroot
+ dh_clean -k
+ dh_installdirs
+
+ find . -type f -not \( -path '*/debian/*' -or -name 'build-stamp' -or -name 'LICENSE.txt' -or -name '.cvsignore' \) -exec install -D --mode=644 {} debian/zope-groupuserfolder/usr/lib/zope/lib/python/Products/GroupUserFolder/{} \;
+
+
+
+
+
+# Build architecture-independent files here.
+binary-indep: DH_OPTIONS=-i
+binary-indep: build install
+ dh_testdir
+ dh_testroot
+ dh_install
+
+
+
+
+ gzip -9 -c ChangeLog > changelog.gz
+ dh_installdocs -A TODO changelog.gz
+ dh_installchangelogs
+
+ dh_link
+ dh_compress
+ dh_fixperms
+ dh_installdeb
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+# Build architecture-dependent files here.
+binary-arch: DH_OPTIONS=-a
+binary-arch: build install
+ dh_testdir
+ dh_testroot
+ dh_install
+
+
+
+
+ gzip -9 -c ChangeLog > changelog.gz
+ dh_installdocs -A TODO changelog.gz
+ dh_installchangelogs
+
+ dh_strip
+ dh_link
+ dh_compress
+ dh_fixperms
+ dh_installdeb
+ dh_shlibdeps
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+binary: binary-indep
+.PHONY: build clean binary-arch binary-indep binary
+
--- /dev/null
+Template: zope-cmfforum/postinst
+Type: boolean
+Default: true
+Description: Do you want me to restart Zope?
+ To let this product/feature work properly, you need to restart Zope. If
+ you want, I may restart Zope automatically, else you should do it your
+ self.
--- /dev/null
+# Example watch control file for uscan
+# Rename this file to "watch" and then you can run the "uscan" command
+# to check for upstream updates and more.
+# Site Directory Pattern Version Script
+ftp.sourceforge.net /pub/sourceforge/collective GroupUserFolder-(.*)\.tar\.gz debian uupdate
--- /dev/null
+Here are the main initial ideas behind GRUF :
+
+ Before we started writing this component, we spent a lot of time on
+the design (yes, using paper and pen ;)), thinking a lot on how to be
+as generic as possible. As a conclusion of our design sessions, we came
+up with the following requirements :
+
+ - a group has to be seen by zope like an user. This way, we can
+guarantee that the _whole_ standard security machinery of Zope will
+continue to work like a charm, without even a hotfix.
+
+ - a first consequence of this is that GRUF will work out of the box
+ with any Zope application, including Plone ;)
+
+ - a second consequence is : groups just have to be stored in
+ a separate acl_users
+
+ - GRUF must be able to handle _any_ existing acl_users component ; including LDAP
+ or sql one
+
+ - GRUF has to be as transparent as possible to applications (read
+ "should act as a normal user folder")
+
+ - Group nesting should be supported
+
+ - Multiple sources for users should be supported (ex : source 1 is
+ SQL, source 2 is LDAP, source 3 is another LDAP).
+
+ The API was designed, test cases were written, code was done,
+documentation was written, first version went out and the first customers
+were (very) happy. Yes, exactly in this order ;)
+
+
+
--- /dev/null
+Can I nest some GRUFs?
+ Maybe... but what for ?
+
+Does GRUF support nested groups ?
+ Nested groups in group-whithin-a-group feature.
+ And, yes, GRUF supports it since 1.3 version.
+
+Does GRUF support multiple user sources ?
+ Multiple user sources is a feature that would allow you to store users in several userfolders.
+ For example, you could have your regular admin users in a standard User Folder, your intranet
+ users in an LDAPUserFolder and your extranet users in an SQL-based user folder
+ GRUF supports this from version 2.0Beta1.
+
+Can I use GRUF outside Plone ?
+ Yes, yes, yes, yes and yes. This is a major design consideration for us.
+
+Is GRUF stable ?
+ It's used in a production environment for several major websites. Furthermore, it's qualified to be included
+ in Plone 1.1. It's considered reliable enough - except for "Beta" versions, of course.
+
+Is GRUF maintained ?
+ Yes, it is, actively. Features (especially regarding useablility) are often
+ added to GRUF. Official releases are considered very stable.
+
+Can I help ?
+ Yes, for sure !
+ GRUF is an Open-Source project and we, at Ingeniweb, are always happy to help people getting involved
+ with our products. Just contact us to submit your ideas, patches or insults ! :-)
+ In any case, if you want to work on GRUF's CVS, please work in a branch, never on the HEAD!
+ I want this to ensure the latest CVS HEAD is always very stable.
+
+Why cannot I assign local roles to groups using Plone 2.0.x ?
+ There's a bug in Plone's folder_localroles_form in Plone 2.0.x, preventing it to work with
+ GRUF 3. That's because group name is passed to GRUF's methods instead of group id.
+ To solve this, you either have to fix the form by yourself (replace group_name by group_id),
+ or wait for Plone 2.1 ;)
+ A sample fixed form is provided in the gruf_plone_2_0 skin folder (which is NOT installed
+ by default).
+
+Does GRUF work with CASUserFolder
+ There are two CASUserFolder implementation. One made by a clown and one made by a megalomaniac ;)
+ I prefer the first one. He prefers me anyway ;) See this page for more information:
+ http://www.zope.org/Members/mrlex/CASUserFolder/CASUserFolder/CAS_and_Zope
--- /dev/null
+GRUF 3.0 is out !
+
+ Abstract
+
+ GRUF 3.0 is out ! This new version brings a lot of API enhancement, along with far better
+ test cases. So, this version is simpler to use for the programmer, and safer to use
+ for end users. And, the cherry on the cake, this version brings a far better LDAP support,
+ especially for large LDAP directories for user searching and listing.
+
+ Link
+
+ Here is the link to <a href="https://sourceforge.net/project/showfiles.php?group_id=55262&package_id=81576&release_id=248008">
+ GRUF 3.0 on Sourceforge</a>.
+
+ What's new ?
+
+ * **New API**, easier to understand and to use (and well-documented in an interface).
+
+ * Complete **LDAPUserFolder** integration, including user creation and user modification.
+
+ * Complete **LDAPUserFolder** integration for **groups**, including group creation and modification!
+
+ * Far better **test case**, with more than... 220 tests, including LDAP tests !
+
+ * Better **Plone** interfacing - this will require Plone 2.1 to work with Plone's
+ management panels.
+
+ What's the future ?
+
+ This version is not fully compatible with Plone2.0 anymore because of the API changes.
+
+ So the next step is to integrate GRUF3 into Plone's next version (namely 2.1). A working
+ branch is already available on <a href="http://svn.plone.org/">SVN</a>: 'pjgrizel-gruf3-branch'.
+ You can patch your Plone2 against this branch if necessary, but this won't be supported!
+
+ Then, GRUF 3.1, which we plan to release this summer, will include **local roles blacklisting**!
+
+
+GRUF 3.0 est sorti !
+
+ Résumé
+
+ GRUF 3.0 est sorti ! Cette version apporte un certain nombre de modifications pour les
+ programmeurs (nouvelle API, plein de nouveaux tests) pour l'environnement Plone, mais aussi
+ et surtout simplifie la configuration et l'interfaçage avec des annuaires LDAP.
+
+ Lien
+
+ Voici le lien vers <a href="https://sourceforge.net/project/showfiles.php?group_id=55262&package_id=81576&release_id=248008">
+ GRUF 3.0 sur Sourceforge</a>.
+
+ Quoi d'neuf ?
+
+ * **Nouvelle API**, plus facile à utiliser et à comprendre (et bien documentée dans des interfaces)
+
+ * Support complet de **LDAPUserFolder**, y compris création et modification d'utilisateurs.
+
+ * Support complet de **LDAPUserFolder** pour les groupes ! Y compris création et modification de
+ groupes.
+
+ * Super **test case** avec plus de 220 tests, y compris des tests avec LDAP.
+
+ * Amélioration de l'interfaçage avec **Plone** pour la gestion des membres et des groupes.
+ Ceci nécessite la version 2.1 de Plone pour fonctionner.
+
+ Et maintenant, qu'est-ce qu'on fait ?
+
+ Cette version de GRUF n'est plus pleinement compatible avec Plone2 (notamment au niveau des
+ pages d'administration des utilisateurs et des groupes) du fait du changement de l'API.
+
+ La prochaine étape est donc d'intégrer GRUF3 à Plone 2.1. Une branche déjà opérationnelle
+ est disponible sur le <a href="http://svn.plone.org/">SVN de Plone</a> : 'pjgrizel-gruf3-branch'.
+ Les grufeurs les plus acharnés prendront un malin plaisir à patcher leur Plone2 avec cette branche,
+ il n'y a pas d'obstacle technique à cette manipulation.
+
+ L'étape suivante est l'intégration du **blacklisting de local rôles** (c'est plus élégant à dire que
+ "noir-listage des rôles locaux") dans GRUF 3.1. Tout ceci sera disponible cet été si la canicule
+ le permet !
+
+
--- /dev/null
+.. figure:: doc/small_menu.png
+ :scale: 30
+ :target: doc/menu.png
+
+ The way GRUF shows-up in the Zope Management Interface
+
+
+.. figure:: doc/small_tab_overview.png
+ :scale: 30
+ :target: doc/tab_overview.png
+
+ Detail of GRUF in ZMI
+
+.. figure:: doc/small_tab_groups.png
+ :scale: 30
+ :target: doc/tab_groups.png
+
+ The overview page with many users
+
+.. figure:: doc/small_tab_groups.png
+ :scale: 30
+ :target: doc/tab_groups.png
+
+ The groups management interface
+
+.. figure:: doc/small_tab_sources.png
+ :scale: 30
+ :target: doc/tab_sources.png
+
+ A sample security audit page
+
+
--- /dev/null
+(Voici le texte d'une interview réalisé par Tarek pour le site zopeur.com)
+
+(Désolé pour le français ;-) )
+
+
+
+
+1) qu'est ce que GRUF ?
+
+ GRUF signifie "GRoup User Folder". Il s'agit d'un User Folder pour
+ Zope capable d'offrir un support pour les groupes. Contrairement aux
+ autres types d'UserFolder se basent sur divers supports (ZODB, SQL,
+ LDAP, ...) pour identifier les utilisateurs, GRUF délègue cette tâche
+ à un UserFolder classique. Par exemple, pour utiliser GRUF avec LDAP,
+ il suffit de coupler GRUF à un LDAPUserFolder tout à fait classique.
+ Cette architecture permet de se dispenser de l'écriture de plugins.
+
+
+2) Quels sont ses particularités / avantages comparé à d'autres produits
+ du genre ?
+
+ Avec GRUF, aucun patch n'est fait dans le code de Zope. GRUF est un
+ UserFolder classique et n'utilise aucune "magie" pour fonctionner.
+
+ Aucun patch dans Zope n'a été nécessaire ; pas même de MonkeyPatch.
+
+ Dans l'interface d'administration de GRUF, on crée deux UserFolders :
+ un pour les groupes et un pour les utilisateurs. Dans l'UserFolder des
+ utilisateurs, le groupes sont affectés aux utilisateurs en tant que
+ rôles.
+
+ Dès que l'on sort de GRUF, en revanche, les groupes sont vus comme des
+ utilisateurs "normaux" sous Zope. On peut leur affecter des droits,
+ des rôles locaux, etc.
+
+ C'est cette "astuce" qui fait que GRUF fonctionne directment avec
+ toutes les applications Zope, sans rien changer au code source !
+
+ L'architecture de GRUF permet d'utiliser des types d'UserFolder
+ classiques comme base d'utilisateurs ou de groupes (le UserFolder
+ standard de Zope mais aussi LDAPUserFolder, ExUserFolder, etc). Pas
+ besoin de développer et de maintenir des PlugIns !
+
+ Autrement dit, GRUF reste simple dans son principe, totalement intégré
+ à Zope (pas de "hotfixing" de Zope), et compatible avec virtuellement
+ tous les types d'UserFolder qui respectent l'API standard de Zope.
+
+ Enfin, un des points forts de GRUF est son plan de tests... Plusieurs
+ centaines de tests pour garantir un maximum de qualité !
+
+
+3) Dans quelle mesure l'outil peut il s'intégrer à un portail Plone ?
+
+ Depuis Plone2, GRUF est partie intégrante de Plone. Des écrans
+ spécifiques ont été développés pour administrer les groupes depuis
+ l'interface de Plone mais en dehors de cet aspect "visuel", aucune
+ adaptation au niveau de la programmation n'a été nécessaire pour
+ rendre Plone compatible avec GRUF.
+
+ Ni pour rendre GRUF compatible Plone, d'ailleurs ;)
+
+ Depuis Plone2, un "tool" est proposé pour rendre la gestion des
+ groupes sous Plone similaire à celle des utilisateurs sous CMF
+ (l'équivalent du MembershipTool, mais pour... les groupes !).
+
+
+4) Et à un autre portail (CMS,Zwook, etc.. ) ? Est-ce que l'outil est
+dédié Plone ?
+
+ Depuis le départ, GRUF est un outil _indépendant_ de Plone. Et nous
+ nous efforçons, à chaque version, de vérifier son bon fonctionnement
+ en dehors de Plone. Puisque GRUF ne modifie rien à la logique de
+ gestion des utilisateurs de Zope, il est donc tout à fait possible de
+ remplacer n'importe quel UserFolder pour bénéficier de la gestion des
+ groupes.
+
+ Il est donc possible, en théorie, de l'utiliser avec ces outils, si
+ ceux-ci n'utilisent pas eux-même du code spécifique à un UserFolder
+ particulier.
+
+
+5) Le futur de GRUF ?
+
+ GRUF3, qui est encore en phase de qualification, propose une nouvelle
+ API beaucoup plus intuitive. Nous avons aussi optimisé certaines
+ routines, notamment pour LDAP (LDAPUserFolder dispose en effet de
+ beaucoup d'optimisations spécifiques).
+
+ GRUF 3 est en phase finale de qualification auprès d'un annuaire de
+ 90.000 utilisateurs ! ;)
+
+ La prochaîne étape dans GRUF sera la possibilité de restreindre des
+ rôles locaux : actuellement, Zope ne permet que d'en ajouter, jamais
+ d'en soustraire - alors que cela pourrait s'avérer bien pratique. Si
+ tout va bien, cela sera implémenté dans les prochaînes semaines.
+ C'est la notion de "BlackList".
+
+ Nous avons également plein d'idées pour rendre les interfaces
+ d'administration des utilisateurs/groupes, que ce soit côté ZMI ou
+ côté Plone, plus intuitives et agréables. Bref, le travail ne manque
+ pas !
+
+ D'ailleurs, n'oublions pas que GRUF est un composant OpenSource, et
+ que, à ce titre, tout le monde peut apporter son grain de sel : code,
+ idées, écrans, doc, traductions, etc...
+
+ Et quoi qu'il en soit, nous devons une fière chandèle à la communauté
+ Plone qui a testé intensivement GRUF, nous a aidé pour certaines
+ parties, nous a envoyé des patches et des idées... C'est là toute la
+ force d'une communauté soudée !
+
--- /dev/null
+<dtml-comment> -*- mode: dtml; dtml-top-element: "body" -*- </dtml-comment>
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<script type="text/javascript">
+<!--
+
+isSelected = false;
+
+function toggleSelect() {
+ if (isSelected == false) {
+ for (i = 0; i < document.objectItems.length; i++)
+ document.objectItems.elements[i].checked = true ;
+ isSelected = true;
+ document.objectItems.selectButton.value = "Deselect All";
+ return isSelected;
+ }
+ else {
+ for (i = 0; i < document.objectItems.length; i++)
+ document.objectItems.elements[i].checked = false ;
+ isSelected = false;
+ document.objectItems.selectButton.value = "Select All";
+ return isSelected;
+ }
+}
+
+//-->
+</script>
+
+<dtml-unless skey><dtml-call expr="REQUEST.set('skey', 'id')"></dtml-unless>
+<dtml-unless rkey><dtml-call expr="REQUEST.set('rkey', '')"></dtml-unless>
+
+<!-- Free text -->
+<dtml-if header_text>
+ <p class="form-help">
+ <dtml-var header_text>
+ </p>
+</dtml-if>
+
+
+<!-- Add object widget -->
+<br />
+<dtml-if filtered_meta_types>
+ <table width="100%" cellspacing="0" cellpadding="0" border="0">
+ <tr>
+ <td align="left" valign="top"> </td>
+ <td align="right" valign="top">
+ <div class="form-element">
+ <form action="&dtml-URL1;/" method="get">
+ <dtml-if "_.len(filtered_meta_types) > 1">
+ <select class="form-element" name=":action"
+ onChange="location.href='&dtml-URL1;/'+this.options[this.selectedIndex].value">
+ <option value="manage_workspace" disabled>Select type to add...</option>
+ <dtml-in filtered_meta_types mapping sort=name>
+ <option value="&dtml.url_quote-action;">&dtml-name;</option>
+ </dtml-in>
+ </select>
+ <input class="form-element" type="submit" name="submit" value=" Add " />
+ <dtml-else>
+ <dtml-in filtered_meta_types mapping sort=name>
+ <input type="hidden" name=":method" value="&dtml.url_quote-action;" />
+ <input class="form-element" type="submit" name="submit" value=" Add &dtml-name;" />
+ </dtml-in>
+ </dtml-if>
+ </form>
+ </div>
+ </td>
+ </tr>
+ </table>
+</dtml-if>
+
+<form action="&dtml-URL1;/" name="objectItems" method="post">
+<dtml-if objectItems>
+<table width="100%" cellspacing="0" cellpadding="2" border="0">
+<tr class="list-header">
+ <td width="5%" align="right" colspan="2"><div
+ class="list-item"><a href="./manage_main?skey=meta_type<dtml-if
+ "rkey == ''">&rkey=meta_type</dtml-if>"
+ onMouseOver="window.status='Sort objects by type'; return true"
+ onMouseOut="window.status=''; return true"><dtml-if
+ "skey == 'meta_type' or rkey == 'meta_type'"
+ ><strong>Type</strong><dtml-else>Type</dtml-if></a></div>
+ </td>
+ <td width="50%" align="left"><div class="list-item"><a
+ href="./manage_main?skey=id<dtml-if
+ "rkey == ''">&rkey=id</dtml-if>"
+ onMouseOver="window.status='Sort objects by name'; return true"
+ onMouseOut="window.status=''; return true"><dtml-if
+ "skey == 'id' or rkey == 'id'"
+ ><strong>Name</strong><dtml-else>Name</dtml-if></a></div>
+ </td>
+ <td width="15%" align="left"><div class="list-item"><a
+ href="./manage_main?skey=get_size<dtml-if
+ "rkey == ''">&rkey=get_size</dtml-if>"
+ onMouseOver="window.status='Sort objects by size'; return true"
+ onMouseOut="window.status=''; return true"><dtml-if
+ "skey == 'get_size' or rkey == 'get_size'"
+ ><strong>Size</strong><dtml-else>Size</dtml-if></a></div>
+ </td>
+ <td width="29%" align="left"><div class="list-item"><a
+ href="./manage_main?skey=bobobase_modification_time<dtml-if
+ "rkey == ''">&rkey=bobobase_modification_time</dtml-if
+ >"
+ onMouseOver="window.status='Sort objects by modification time'; return true"
+ onMouseOut="window.status=''; return true"><dtml-if
+ "skey == 'bobobase_modification_time' or rkey == 'bobobase_modification_time'"
+ ><strong>Last Modified</strong><dtml-else>Last Modified</dtml-if></a></div>
+ </td>
+</tr>
+<dtml-in objectItems sort_expr="skey" reverse_expr="rkey">
+<dtml-if sequence-odd>
+<tr class="row-normal">
+<dtml-else>
+<tr class="row-hilite">
+</dtml-if>
+ <td align="left" valign="top" width="16">
+ <input type="checkbox" name="ids:list" value="&dtml-sequence-key;" />
+ </td>
+ <td align="left" valign="top" nowrap="1">
+
+ <dtml-if om_icons>
+ <a href="&dtml.url_quote-sequence-key;/manage_workspace">
+ <dtml-in om_icons mapping>
+ <img src="&dtml-BASEPATH1;/&dtml.url_quote-path;" alt="&dtml.missing-alt;"
+ title="&dtml.missing-title;" border="0" /></dtml-in></a>
+ <dtml-else>
+
+ <dtml-if icon>
+ <a href="&dtml.url_quote-sequence-key;/manage_workspace">
+ <img src="&dtml-BASEPATH1;/&dtml-icon;" alt="&dtml-meta_type;"
+ title="&dtml-meta_type;" border="0" /></a>
+ <dtml-else>
+
+ </dtml-if>
+
+ </dtml-if>
+
+ </td>
+ <td align="left" valign="top">
+ <div class="list-item">
+ <a href="&dtml.url_quote-sequence-key;/manage_workspace">
+ &dtml-sequence-key; <dtml-if title>(&dtml-title;)</dtml-if>
+ </a>
+ <dtml-if locked_in_version>
+ <dtml-if modified_in_version>
+ <img src="&dtml-BASEPATH1;/p_/locked"
+ alt="This item has been modified in this version" />
+ <dtml-else>
+ <img src="&dtml-BASEPATH1;/p_/lockedo"
+ alt="This item has been modified in another version" />
+ (<em>&dtml-locked_in_version;</em>)
+ </dtml-if>
+ </dtml-if>
+ </div>
+ </td>
+
+ <dtml-with sequence-key>
+ <td>
+ <div class="list-item">
+ <dtml-try>
+ <dtml-if get_size>
+ <dtml-let ob_size=get_size>
+ <dtml-if "ob_size < 1024">
+ 1 Kb
+ <dtml-elif "ob_size > 1048576">
+ <dtml-var "ob_size / 1048576.0" fmt="%0.02f"> Mb
+ <dtml-else>
+ <dtml-var "_.int(ob_size / 1024)"> Kb
+ </dtml-if>
+ </dtml-let>
+ <dtml-else>
+
+ </dtml-if>
+ <dtml-except>
+
+ </dtml-try>
+ </div>
+ </td>
+
+ <td>
+ <div class="list-item">
+ <dtml-var bobobase_modification_time fmt="%Y-%m-%d %H:%M">
+ </div>
+ </td>
+ </dtml-with>
+</tr>
+</dtml-in>
+</table>
+
+<table cellspacing="0" cellpadding="2" border="0">
+<tr>
+ <td align="left" valign="top" width="16"></td>
+ <td align="left" valign="top">
+ <div class="form-element">
+ <dtml-unless dontAllowCopyAndPaste>
+ <input class="form-element" type="submit" name="manage_renameForm:method"
+ value="Rename" />
+ <input class="form-element" type="submit" name="manage_cutObjects:method"
+ value="Cut" />
+ <input class="form-element" type="submit" name="manage_copyObjects:method"
+ value="Copy" />
+ <dtml-if cb_dataValid>
+ <input class="form-element" type="submit" name="manage_pasteObjects:method"
+ value="Paste" />
+ </dtml-if>
+ </dtml-unless>
+ <dtml-if "_.SecurityCheckPermission('Delete objects',this())">
+ <input class="form-element" type="submit" name="manage_delObjects:method"
+ value="Delete" />
+ </dtml-if>
+ <dtml-if "_.SecurityCheckPermission('Import/Export objects', this())">
+ <input class="form-element" type="submit"
+ name="manage_importExportForm:method"
+ value="Import/Export" />
+ </dtml-if>
+<script type="text/javascript">
+<!--
+if (document.forms[0]) {
+ document.write('<input class="form-element" type="submit" name="selectButton" value="Select All" onClick="toggleSelect(); return false">')
+ }
+//-->
+</script>
+ </div>
+ </td>
+</tr>
+</table>
+
+<dtml-else>
+<table cellspacing="0" cellpadding="2" border="0">
+<tr>
+<td>
+<div class="std-text">
+There are currently no items in <em>&dtml-title_or_id;</em>
+<br /><br />
+</div>
+<dtml-unless dontAllowCopyAndPaste>
+<dtml-if cb_dataValid>
+<div class="form-element">
+<input class="form-element" type="submit" name="manage_pasteObjects:method"
+ value="Paste" />
+</div>
+</dtml-if>
+</dtml-unless>
+<dtml-if "_.SecurityCheckPermission('Import/Export objects', this())">
+<input class="form-element" type="submit"
+ name="manage_importExportForm:method" value="Import/Export" />
+</dtml-if>
+</td>
+</tr>
+</table>
+</dtml-if>
+</form>
+
+<dtml-if update_menu>
+<script type="text/javascript">
+<!--
+window.parent.update_menu();
+//-->
+</script>
+</dtml-if>
+
+<dtml-var manage_page_footer>
+
+
+
+
+
+
+
+
+
+
+
+
+
--- /dev/null
+ <h1 tal:define="global print request/pp | nothing"></h1>
+ <h1 tal:replace="structure here/manage_page_header">Header</h1>
+ <h2 tal:condition="not: print" tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+ tal:replace="structure here/manage_tabs">Tabs</h2>
+
+
+ <div tal:condition="request/doIt | nothing">
+ <h4>Audit results</h4>
+
+ <table
+ border="1"
+ class="list-item"
+ tal:define="
+ global users_and_roles here/listUsersAndRoles;
+ site_tree here/getSiteTree;
+ table_cache python:here.computeSecuritySettings(site_tree, users_and_roles, [('R', request.read_permission), ('W', request.write_permission)]);
+ "
+ tal:condition="users_and_roles"
+ >
+ <tr tal:define="width python:int(100/len(users_and_roles))">
+ <td width="0" tal:attributes="width string:$width%"></td>
+ <td width="0" align="center"
+ tal:repeat="s users_and_roles"
+ tal:attributes="width string:$width%"
+ >
+ <span tal:define="color python:test(s[0] == 'user', here.user_color, test(s[0] == 'group', here.group_color, here.role_color))">
+ <font color="" tal:attributes="color color">
+ <tal:block tal:condition="not:request/use_legend|nothing">
+ <b tal:content="structure python:s[4]" /><br />
+ </tal:block>
+ <tal:block tal:condition="request/use_legend|nothing">
+ <b tal:content="python:s[3]" />
+ </tal:block>
+ </font>
+ <span tal:condition="not:request/use_legend|nothing">
+ (<font color="" tal:attributes="color color"><span tal:replace="python:s[0]" /></font>)
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr tal:repeat="folder site_tree">
+ <td nowrap="1">
+ <span tal:repeat="x python:range(0,folder[1])" tal:omit-tag="">-</span>
+ <a href=""
+ tal:attributes="href python:folder[2]"
+ tal:content="python:folder[0]"
+ />
+ <tal:block
+ tal:define="state python:here.portal_workflow.getInfoFor(here.restrictedTraverse(folder[2]), 'review_state')"
+ tal:on-error="nothing"
+ >
+ <br />
+ <span tal:repeat="x python:range(0,folder[1])" tal:omit-tag="">-</span>
+ <span tal:replace="state" />
+ </tal:block>
+ </td>
+ <td
+ tal:repeat="s users_and_roles"
+ >
+ <tal:block
+ tal:define="
+ R python:table_cache[folder[2]][s[:2]].get('R', None);
+ W python:table_cache[folder[2]][s[:2]].get('W', None)"
+ >
+ <span tal:condition="R">R</span>
+ <span tal:condition="W">W</span>
+ <span tal:condition="python: (not R) and (not W)"> </span>
+ </tal:block>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <div tal:condition="request/use_legend|nothing">
+ <h4>Legend</h4>
+ <ol>
+ <table>
+ <tr class="list-header">
+ <th class="list-header">Id</th>
+ <th class="list-header">Label</th>
+ <th class="list-header">Kind</th>
+ </tr>
+
+ <tr tal:repeat="actor users_and_roles">
+ <span tal:define="color python:test(actor[0] == 'user', here.user_color, test(actor[0] == 'group', here.group_color, here.role_color))">
+ <td class="list-item"><font color="" tal:attributes="color color" tal:content="python:actor[3]">Id</font></td>
+ <td class="list-item"><font color="" tal:attributes="color color" tal:content="structure python:actor[4]">Label</font></td>
+ <td class="list-item"><font color="" tal:attributes="color color" tal:content="python:actor[0]">Kind</font></td>
+ </span>
+ </tr>
+
+ </table>
+ </ol>
+ </div>
+
+ <div tal:condition="not: print" tal:omit-tag="">
+ <h4>Audit settings</h4>
+ <ol>
+ <p>
+ See help below if you do not understand those settings.
+ </p>
+
+ <form action="manage_audit" method="GET">
+ <input type="hidden" name="doIt" value="1">
+ <table
+ tal:define="default here/getDefaultPermissions"
+ >
+ <tr class="list-header">
+ <th>Parameter</th>
+ <th class="list-header">Setting</th>
+ </tr>
+ <tr>
+ <td><div class="list-item">Read permission</div></td>
+ <td>
+ <select name="read_permission" size="1">
+ <option
+ selected=0
+ value=""
+ tal:repeat="perm here/listAuditPermissions"
+ tal:attributes="
+ value perm;
+ selected python:perm == default['R'];
+ "
+ tal:content="perm"
+ />
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td><div class="list-item">Write permission</div></td>
+ <td>
+ <select name="write_permission" size="1">
+ <option
+ selected=0
+ value=""
+ tal:repeat="perm here/listAuditPermissions"
+ tal:attributes="
+ value perm;
+ selected python:perm == default['W'];
+ "
+ tal:content="perm"
+ />
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td><div class="list-item">Displayed actors</div></td>
+ <td>
+ <div class="list-item">
+ <input type="checkbox" name="display_roles" checked="" tal:attributes="checked request/display_roles|python:test(request.get('doIt',None), 0, 1)">
+ <font color="" tal:attributes="color here/role_color">Roles</font><br />
+ <input type="checkbox" name="display_groups" checked="" tal:attributes="checked request/display_groups|python:test(request.get('doIt',None), 0, 1)">
+ <font color="" tal:attributes="color here/group_color">Groups</font><br />
+ <input type="checkbox" name="display_users" checked="" tal:attributes="checked request/display_users|python:test(request.get('doIt',None), 0, 0)">
+ <font color="" tal:attributes="color here/user_color">Users</font>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top"><div class="list-item">Use a legend</div></td>
+ <td>
+ <div class="list-item">
+ <input type="checkbox" name="use_legend" checked="" tal:attributes="checked request/use_legend|nothing">
+ (Use this feature to display actors names outside the table. This will reduce the table width, which may be useful for printing, for example.)
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td><div class="list-item">Printable page</div></td>
+ <td>
+ <div class="list-item">
+ <input type="checkbox" name="pp" checked="" tal:attributes="checked request/pp|nothing">
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td><input type="submit" value="View"></td>
+ </tr>
+ </table>
+ </form>
+ </ol>
+ </div>
+
+
+ <div tal:condition="not: print" tal:omit-tag="">
+ <div tal:condition="not:request/doIt | nothing">
+
+ <h4>About the audit table</h4>
+ <ol>
+ <p>
+ This management tab allows one to check how the site security is applied for the most useful cases.<br />
+ This allows you to have a precise abstract of the security settings for a little set of permissions as
+ if it simply were "Read" and "Write" permissions.
+ </p>
+
+ <p>
+ <strong>
+ This management tab won't change anything in your security settings. It is just intended to show information and not to modify anything.
+ </strong>
+ </p>
+
+ <p>
+ Select, in the form below, the permissions you want to monitor and the kind of actors (roles, groups or users) you want to display.
+ </p>
+
+ <ol>
+ <h4>Hint</h4>
+ <p>
+ Usually, for a regular Zope site, the
+ permission set would be mapped this way:
+ </p>
+
+ <ul>
+ <li>Read: View</li>
+ <li>Write: Change Images and Files</li>
+ </ul>
+ <p>
+ For a Plone site, the
+ permission set would be mapped this way:
+ </p>
+
+ <ul>
+ <li>Read: View</li>
+ <li>Write: Modify portal content</li>
+ </ul>
+ <p>
+ If you have <strong>a lot of users</strong>, rendering this audit can be very time-consuming.<br />
+ In such conditions, you can select only "roles" to make things a lot faster.
+ </ol>
+ </ol>
+ </div>
+ </div>
+
+ <h1 tal:replace="structure here/manage_page_footer">Footer</h1>
--- /dev/null
+<h1 tal:replace="structure here/manage_page_header">Header</h1>
+<h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+ tal:replace="structure here/manage_tabs">Tabs</h2>
+
+ <ol>
+ <p class="form-help">
+ You are currently running <strong>GRUF v.<span tal:replace="here/getGRUFVersion">version</span></strong><br />
+ Information, latest version, documentation... see
+ <a target="_blank" href="http://ingeniweb.sourceforge.net/Products/GroupUserFolder">The GRUF Webpage</a>.
+ </p>
+ </ol>
+
+ <!-- Show problems if it happens -->
+ <div tal:condition="request/GRUF_PROBLEM|nothing">
+ <font color="red"><strong><span tal:content="request/GRUF_PROBLEM">gruf message</span></strong></font>
+ </div>
+
+
+ <h4>Users folders management</h4>
+ <ol>
+ <p class="form-help">Use this form to check/manage the underlying user folders.</p>
+ <p class="form-help">BE CAREFUL THAT MISUSE OF THIS FORM CAN LEAD YOU TO UNRECOVERABLE LOSS OF USER DATA.</p>
+ <p class="form-help">For this reason, all destructive actions (ie. replacing or deleting) with existing UserFolders must be confirmed
+ by clicking the rightmost checkbox.</p>
+
+ <form action="" tal:attributes="action string:${here/absolute_url}" method="POST">
+ <!-- Users selection -->
+ <table bgcolor="#EEEEEE" tal:on-error="nothing">
+ <tr>
+ <td rowspan="2" valign="middle"></td>
+ <th class="list-header" rowspan="2" valign="middle">Type</th>
+ <th class="list-header" colspan="5">Actions</th>
+ </tr>
+ <tr class="list-header">
+ <th>Move</th>
+ <th>Enable</th>
+ <th>Replace</th>
+ <th>Delete</th>
+ <th>Confirm</th>
+ </tr>
+
+ <!-- Groups source row -->
+ <tr>
+ <th class="list-header">Groups source</th>
+ <td bgcolor="#EEEEEE">
+ <img src="" tal:attributes="src here/Groups/acl_users/icon">
+ <a href="Groups/acl_users/manage_workspace" tal:content="here/Groups/acl_users/meta_type">Type</a>
+ </td>
+ <td></td>
+ <td bgcolor="#EEEEEE"> </td>
+ <td bgcolor="#EEEEEE">
+ <table border="0">
+ <tr>
+ <td align="left">
+ <input type="hidden" name="source_rec.id:records" value="Groups" />
+ <select name="source_rec.new_factory:records">
+ <option value="">-- Select your source type --</option>
+ <tal:block tal:repeat="source here/listAvailableUserSources">
+ <option value=""
+ tal:condition="python:source[0] != path('here/Groups/acl_users/meta_type')"
+ tal:attributes="value python:source[1]">
+ <span tal:replace="python:source[0]">name</span>
+ </option>
+ </tal:block>
+ </select>
+ </td>
+ <td align="right">
+ <input type="submit" name="replaceUserSource:action" value="Ok" />
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td class="list-item">(forbidden)</td>
+ <td bgcolor="#EEEEEE" class="list-item">
+ <input type="checkbox" name="id" value="Groups" />I'm sure
+ </td>
+ </tr>
+
+
+ <!-- Users sources row -->
+ <tr tal:repeat="source here/listUserSourceFolders">
+ <th class="list-header">Users source #<span tal:replace="repeat/source/number">1</span></th>
+ <td bgcolor="#EEEEEE" tal:condition="source/isValid">
+ <img src=""
+ tal:attributes="src source/acl_users/icon;
+ title source/acl_users/meta_type;">
+ <a href=""
+ tal:attributes="
+ href string:${source/acl_users/absolute_url}/manage_workspace;
+ title source/acl_users/meta_type;"
+ tal:content="source/acl_users/title|source/acl_users/meta_type">Type</a>
+ <tal:block condition="not:source/isEnabled">
+ <font color="red"><i>(disabled)</i></font>
+ </tal:block>
+ </td>
+ <td bgcolor="#EEEEEE" tal:condition="not:source/isValid">
+ <font color="red"><strong><i>(invalid or broken)</i></strong></font>
+ </td>
+ <td bgcolor="#EEEEEE" align="center">
+ <a tal:condition="not:repeat/source/start"
+ tal:attributes="href string:${here/absolute_url}/moveUserSourceUp?id=${source/getUserSourceId}"
+ href=""><img src="img_up_arrow" border="0" alt="Move up"></a>
+ <span tal:condition="repeat/source/start"><img src="img_up_arrow_grey" border="0" alt="Move up"></span>
+
+ <a tal:condition="not:repeat/source/end"
+ tal:attributes="href string:${here/absolute_url}/moveUserSourceDown?id=${source/getUserSourceId}"
+ href=""><img src="img_down_arrow" border="0" alt="Move down"></a>
+ <span tal:condition="repeat/source/end"><img src="img_down_arrow_grey" border="0" alt="Move down"></span>
+ </td>
+ <td bgcolor="#EEEEEE">
+ <font size="-2">
+ <a
+ tal:condition="source/isEnabled"
+ tal:attributes="href string:${here/absolute_url}/toggleSource?src_id=${source/getUserSourceId}"
+ >Disable
+ </a>
+ <a
+ tal:attributes="href string:${here/absolute_url}/toggleSource?src_id=${source/getUserSourceId}"
+ tal:condition="not: source/isEnabled"
+ >Enable
+ </a>
+ </font>
+ </td>
+ <td bgcolor="#EEEEEE">
+ <table border="0">
+ <tr>
+ <td align="left">
+ <input type="hidden" name="source_rec.id:records" value="" tal:attributes="value source/getUserSourceId" />
+ <select name="source_rec.new_factory:records">
+ <option value="">-- Select your source type --</option>
+ <tal:block tal:repeat="new_source here/listAvailableUserSources">
+ <option value=""
+ tal:condition="python:new_source[0] != path('source/acl_users/meta_type')"
+ tal:attributes="value python:new_source[1]">
+ <span tal:replace="python:new_source[0]">name</span>
+ </option>
+ </tal:block>
+ </select>
+ </td>
+ <td align="right">
+ <input type="submit" name="replaceUserSource:action" value="Ok" />
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td bgcolor="#EEEEEE" tal:condition="python:repeat['source'].length > 1" class="list-item">
+ <input
+ type="submit"
+ name="deleteUserSource:action"
+ value="Delete" />
+ </td>
+ <td tal:condition="python:not repeat['source'].length > 1" class="list-item">
+ (forbidden)
+ </td>
+ <td bgcolor="#EEEEEE" class="list-item">
+ <input type="checkbox" name="id" value="" tal:attributes="value source/getUserSourceId" />I'm sure
+ </td>
+ </tr>
+
+
+ <!-- Blank row -->
+ <tr>
+ <td class="list-item" colspan="6"> </td>
+ </tr>
+
+ <!-- New sources row -->
+ <tr>
+ <th class="list-header">Add...</th>
+ <td colspan="6" class="list-item">
+ <select name="factory_uri">
+ <option value="">-- Select your source type --</option>
+ <option value="" tal:repeat="source here/listAvailableUserSources" tal:attributes="value python:source[1]">
+ <span tal:replace="python:source[0]">name</span>
+ </option>
+ </select>
+ <input type="submit" name="addUserSource:method" value="Add" />
+ </td>
+ </tr>
+ </table>
+ </form>
+
+ </ol>
+
+ <tal:block condition="here/hasLDAPUserFolderSource">
+ <h4>Special operations</h4>
+ <ol>
+ <p class="form-help">
+ To manage groups with a LDAPUserFolder, one must map LDAP groups to Zope Roles.<br />
+ You can do this mapping manually or click this button to have it done automatically.<br />
+ Please not that any previously existing ldap-group - to - zope-role mapping may be lost.
+ </p>
+ <p class="form-help">
+ To help you in this task, you can have a look at the following table, which summs up<br />
+ the mappings done (or not done!) in LDAPUserFolder.
+ </p>
+
+ <table>
+ <thead>
+ <th>GRUF group</th>
+ <th>LDAP group</th>
+ </thead>
+ <tbody>
+ <tr tal:repeat="group_info here/listLDAPUserFolderMapping">
+ <td tal:content="python:group_info[0]"></td>
+ <td tal:content="python:group_info[1]"></td>
+ </tr>
+ </tbody>
+ </table>
+ <form action="updateLDAPUserFolderMapping">
+ <input type="submit" value="Update LDAP mapping" />
+ </form>
+ </ol>
+ </tal:block>
+
+
+<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
--- /dev/null
+<h1 tal:replace="structure here/manage_page_header">Header</h1>
+<h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+ tal:replace="structure here/manage_tabs">Tabs</h2>
+
+ <h4>Groups sources</h4>
+ <!-- Groups source row -->
+ <ol>
+ <table cellspacing="10" width="90%" tal:define="groups here/getGroups">
+ <tr>
+ <th class="list-header">Groups source</th>
+ <td bgcolor="#EEEEEE">
+ <img src="" tal:attributes="src here/Groups/acl_users/icon">
+ <a href="Groups/acl_users/manage_workspace" tal:content="here/Groups/acl_users/meta_type">Type</a>
+ </td>
+ </tr>
+ </table>
+ </ol>
+
+ <h4>Groups management</h4>
+ <form action="" method="POST" tal:attributes="action here/absolute_url">
+ <ol>
+ <table cellspacing="10" width="90%" tal:define="groups here/getGroups">
+ <tr>
+ <!-- Groups selection -->
+ <td valign="top">
+ <table bgcolor="#EEEEEE" width="100%">
+ <tr class="list-header" tal:condition="groups">
+ <th> </th>
+ <th>Group</th>
+ <th class="list-header">Member <br>of groups</th>
+ <th class="list-header">Implicitly <br>member of*</th>
+ <th class="list-header">Has roles</th>
+ <th class="list-header">Implicitly <br>has roles**</th>
+ </tr>
+
+ <tr
+ tal:repeat="group groups" class="" tal:attributes="class python:test(path('repeat/group/odd'), 'row-hilite', 'row-normal')"
+ >
+ <div tal:define="
+ label_groups python:group.getGroups();
+ label_groups_no_recurse python:group.getImmediateGroups();
+ label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups);
+ groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse);
+ groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse);
+ roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), group.getRoles());
+ roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), group.getUserRoles());
+ roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles);"
+ tal:omit-tag="">
+ <td><div class="list-item"><input type="checkbox" name="groups:list" value="" tal:attributes="value group"></td>
+ <td>
+ <div class="list-item">
+ <img src="img_group">
+ <strong tal:content="structure group/asHTML">
+ </strong>
+ </td>
+ <td class="list-item">
+ <span tal:repeat="group groups_no_recurse" >
+ <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ <td class="list-item">
+ <span tal:repeat="group groups_recurse" >
+ <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_no_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ </div>
+ </tr>
+
+ <!-- New user -->
+ <tr>
+ <td><div class="list-item"> </div></td>
+ <td><div class="list-item">Create groups:<br /><textarea name="new_groups:lines" cols="20" rows="3"></textarea></div></td>
+ <td colspan="4">
+ <div class="list-item">
+ Newly created groups will be affected groups and roles according to the table below.
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" align="center">
+ <input type="submit" name="changeOrCreateGroups:method" value="Create" />
+
+ <input type="submit" name="deleteGroups:method" value="Delete" />
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td align="center">
+ <div class="list-item">
+ Select one or more users in the upper table, select one or more groups / roles in the table below
+ and click "Change" to affect groups / roles to these users.
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top" align="center" colspan="6">
+ <table bgcolor="#EEEEEE">
+ <tr>
+ <td valign="top">
+ <!-- Groups selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Affect groups</th>
+ </tr>
+
+ <tr tal:repeat="group here/getGroups">
+ <td>
+ <input type="checkbox" name="nested_groups:list" value="" tal:attributes="value group">
+ </td>
+ <td>
+ <div class="list-item" tal:content="structure group/asHTML"></div>
+ </td>
+ </tr>
+
+ <!-- "(None)" item -->
+ <tr>
+ <td><div class="list-item"><input type="checkbox" name="nested_groups:list" value="__None__"></div></td>
+ <td><div class="list-item"><i>(None)</i></div></td>
+ </tr>
+ </table>
+ </td>
+ <td valign="top">
+ <!-- Roles selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Affect roles</th>
+ </tr>
+
+ <tr tal:repeat="role here/valid_roles">
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <input type="checkbox" name="roles:list" value="" tal:attributes="value role">
+ </td>
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div>
+ </td>
+ </tr>
+
+ <!-- "(None)" item -->
+ <tr>
+ <td><div class="list-item"><input type="checkbox" name="roles:list" value="__None__"></div></td>
+ <td><div class="list-item"><i>(None)</i></div></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" align="middle"><input type="submit" name="changeOrCreateGroups:method" value="Change" /></td>
+ </table>
+ </td>
+ </tr>
+ </table>
+
+
+
+
+ <tr tal:replace="nothing">
+ <td valign="top" bgcolor="#EEEEEE">
+ <!-- Groups selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Affect groups</th>
+ </tr>
+
+ <tr tal:repeat="group here/getGroups">
+ <td>
+ <input type="checkbox" name="nested_groups:list" value="" tal:attributes="value group">
+ </td>
+ <td>
+ <div class="list-item" tal:content="structure group/asHTML"></div>
+ </td>
+ </tr>
+
+ <!-- "(None)" item -->
+ <tr>
+ <td><div class="list-item"><input type="checkbox" name="nested_groups:list" value="__None__"></div></td>
+ <td><div class="list-item"><i>(None)</i></div></td>
+ </tr>
+ </table>
+
+ <br>
+
+ <!-- Roles selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Affect roles</th>
+ </tr>
+
+ <tr tal:repeat="role here/valid_roles">
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <input type="checkbox" name="roles:list" value="" tal:attributes="value role">
+ </td>
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div>
+ </td>
+ </tr>
+ <!-- "(None)" item -->
+ <tr>
+ <td><div class="list-item"><input type="checkbox" name="roles:list" value="__None__"></div></td>
+ <td><div class="list-item"><i>(None)</i></div></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <p class="form-help">
+ * According to the groups inheritance, this group is also recursively member of these groups. <br />This is what we call nested groups.
+ </p>
+ <p class="form-help">
+ ** Accorded to the groups inheritance, this group also has these roles - even if they are not defined explicitly on it.
+ </p>
+
+ </ol>
+ </form>
+
+
+ <h4>Instructions</h4>
+ <ol>
+
+ <p class="form-help">
+ To change roles for one or several groups, select them in the left form, select the roles you want to give them in the form on the right and click "Change".<br />
+ You can also create one or several groups by filling the text area (one group per line). the "Change" button will create them with the roles you've selected.<br />
+ If you are fed up with some groups, you can delete them by selecting them and clicking the "Delete" button.
+ </p>
+ <p class="form-help">
+ If you do not select any role, roles won't be reseted for the selected groups.<br />
+ If you do not select any group, groups won't be reseted for the selected groups.<br />
+ To explicitly reset groups or roles, just click the "(None)" entry (and no other entry).
+ </p>
+ </ol>
+
+ <h4>Important notice / disclaimer</h4>
+
+ <ol>
+ <p class="form-help">
+ This form uses the regular Zope Security API from the underlying user folders. However, you may experience problems with some
+ of them, especially if they are not tuned to allow user adding. For example, an LDAPUserFolder can be configured to disable
+ users management. In case this form doesn't work, you'll have to do things by hand within the 'Users' and 'Groups' GRUF folders.
+ </p>
+
+ <p class="form-help">
+ This is not a GRUF limitation ! :-)
+ </p>
+ </ol>
+
+<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
--- /dev/null
+ <h1 tal:replace="structure here/manage_page_header">Header</h1>
+
+ <p class="form-help">
+ This form appear because you've just created some users.<br />
+ GRUF has generated random passwords for them: here they are.
+ </p>
+
+ <p class="form-help">
+ <b><font color="red">IMPORTANT</font></b>: Take some time to write down this information
+ (a copy/paste within a notepad should do it) before clicking the "Ok" button below, as
+ you won't have any (easy) way to retreive your user's passwords after!
+ </p>
+
+ <h4>Generated passwords</h4>
+ <ol>
+ <form action="" method="GET" tal:attributes="action string:${here/absolute_url}/manage_users">
+ <div tal:repeat="user request/USER_PASSWORDS">
+ <span tal:content="user/name">User name</span> :
+ <span class="list-item" tal:content="user/password">
+ </span>
+ </div>
+
+
+ <!-- Actions -->
+ <p align="left">
+ <input type="submit" name="changeOrCreateGroups:method" value="Ok" />
+ </p>
+ </form>
+ </ol>
+
+
+ <h1 tal:replace="structure here/manage_page_footer">Footer</h1>
--- /dev/null
+<div tal:replace="nothing"> -*- mode: dtml; dtml-top-element: "body" -*- </div>
+<div tal:replace="structure here/manage_page_header"></div>
+<div tal:replace="structure here/manage_tabs"></div>
+
+
+<!-- Help text -->
+<p class="form-help">Here is an overview of users, their groups and roles. See the legend below.</p>
+
+<h4>About GRUF</h4>
+ <ol>
+ <p class="form-help">
+ You are currently running <strong>GRUF v.<span tal:replace="here/getGRUFVersion">version</span></strong><br />
+ Information, latest version, documentation... see
+ <a target="_blank" href="http://ingeniweb.sourceforge.net/Products/GroupUserFolder">The GRUF Webpage</a>.
+ </p>
+ </ol>
+
+<!-- Wizards -->
+<h4>What do you want to do from here ?</h4>
+<ol>
+ <p class="form-help">
+ Here is the list of common actions you can do with GRUF. <br />
+ Just follow the links !
+ </p>
+
+
+ <table width="90%">
+ <tr>
+ <th class="list-header" valign="top" width="30%">
+ I want to set the place where
+ my users/groups are stored.
+ </th>
+ <td class="list-item" valign="top" bgcolor="#EEEEEE">
+ <p>
+ Within GRUF, users are stored in one or more <i>User Source</i>. A source can be any
+ valid Zope User Folder derived object (for example the standard Zope User Folder but also LDAPUserFolder,
+ SimpleUserFolder, ...).<br />
+ Use the <strong><a href="manage_GRUFSources">sources tab</a></strong> to manage your user sources.
+ </p>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header" valign="top">
+ I want to connect my LDAP server to Plone
+ </th>
+ <td class="list-item" valign="top" bgcolor="#EEEEEE">
+ <p>
+ There are a few tasks you can automate with Plone (2.0.x or 2.1) in the <strong><a href="manage_wizard">LDAP Wizard</a></strong> section.
+ </p>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header" valign="top">
+ I want to create some users or some groups.
+ </th>
+ <td class="list-item" valign="top" bgcolor="#EEEEEE">
+ <p>
+ To create groups, use the <strong><a href="manage_groups">groups tab</a></strong><br />
+ If you want to create users, you can use the <strong><a href="manage_users">users tab</a></strong>
+ </p>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header" valign="top">
+ I need to check my website's security.
+ </th>
+ <td class="list-item" valign="top" bgcolor="#EEEEEE">
+ <p>
+ The <strong><a href="manage_audit">audit tab</a></strong> is certainly what you are looking for.<br />
+ With this tool you can issue personalized reports about your website security rules.
+ </p>
+ </td>
+ </tr>
+ </table>
+</ol>
+
+
+<!-- Users / Roles / Groups tabular view -->
+<h4>Users overview</h4>
+<ol>
+ <p class="form-help">
+ There may be more users in your system than the ones presented here.
+ See the <a href="manage_users">users tab</a> for more information.
+ </p>
+ <tal:block
+ tal:define="
+ global batch python:test(request.has_key('start'), 0, here.listUsersBatchTable());
+ global start python:request.get('start', 0);
+ "
+ ></tal:block>
+
+ <tal:block tal:condition="batch">
+ <p class="form-help">
+ To avoid too much overhead on this display, it is not possible to show more than 100 users
+ per screen. Please click the range of users you want to see in the table below.
+ </p>
+
+ <table tal:replace="nothing" cellpadding="2" width="90%">
+ <tr tal:repeat="rows batch">
+ <td width="25%" bgcolor="#DDDDDD" tal:repeat="col rows">
+ <table height="100%" width="100%" bgcolor="#FFFFFF">
+ <tr>
+ <td nowrap="1" align="center">
+ <div class="list-item">
+ <a href=""
+ tal:attributes="href python:'%s/manage_overview?start:int=%d' % (here.absolute_url(), col[0])">
+ <img src="img_user" border="0" align="middle"><span tal:replace="python:col[2]" /> ...
+ <span tal:replace="python:col[3]" />
+ </a>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </tal:block>
+
+ <tal:block tal:condition="not:batch">
+ <tal:block tal:define="users python:here.getUsersBatch(start)">
+ <table width="90%" tal:condition="users">
+ <tr class="list-header">
+ <th>User</th>
+ <th>Group(s)</th>
+ <th>Role(s)</th>
+ </tr>
+
+ <tal:block tal:repeat="user users">
+ <tr class="row-hilite"
+ tal:define="
+ label_groups python:user.getGroups();
+ label_groups_no_recurse python:user.getGroups(no_recurse = 1);
+ label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups);
+ groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse);
+ groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse);
+ roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles());
+ roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles());
+ roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles)"
+ >
+ <td>
+ <div class="list-item">
+ <img src="img_user"> <strong tal:content="structure user/asHTML"></strong>
+ </div>
+ </td>
+ <td>
+ <!-- Groups -->
+ <div class="list-item">
+ <span tal:repeat="group groups_no_recurse"
+ ><span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span></span
+ ><span tal:condition="python:groups_no_recurse and groups_recurse">,</span>
+ <span tal:repeat="group groups_recurse" >
+ <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </div>
+ </td>
+ <td>
+ <!-- Roles -->
+ <div class="list-item">
+ <span tal:repeat="role roles_no_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ <span tal:condition="python:roles_no_recurse and roles_recurse">, </span>
+ <span tal:repeat="role roles_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ </tr>
+ </tal:block>
+ </table>
+
+ <table tal:condition="not:users">
+ <tr>
+ <td class="row-hilite" colspan="3">
+ <p>
+ No user available. This happens either if you have no users defined or if
+ the underlying UserFolder cannot retreive the entire users list.
+ </p>
+ </td>
+ </tr>
+ </table>
+ </tal:block>
+ </tal:block>
+</ol>
+
+
+<!-- Legend -->
+<h4>Legend</h4>
+<ol>
+ <p>
+ Just to make things clearer: <br>
+ <font color="" tal:attributes="color here/user_color"><img src="img_user"> Users appear this way</font><br />
+ <font color="" tal:attributes="color here/group_color"><img src="img_group"> Groups appear this way</font><br />
+ <font color="" tal:attributes="color here/group_color"><i><img src="img_group"> Nested groups (ie. groups inside groups) appear this way</i></font><br />
+ <font color="" tal:attributes="color here/role_color">User roles appear this way</font><br />
+ <font color="" tal:attributes="color here/role_color"><i>Nested roles (ie. roles set on a group a user or group belongs to) appear this way</i></font><br />
+ </p>
+ <p class="form-help">In management forms, items only non-italic items can be set/unset directly. Italic items are dependencies.</p>
+</ol>
+
+<dtml-var manage_page_footer>
+
--- /dev/null
+ <h1 tal:replace="structure here/manage_page_header">Header</h1>
+ <h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+ tal:replace="structure here/manage_tabs">Tabs</h2>
+ <tal:block tal:define="
+ global user python:here.getUser(request.username);
+ kind python:test(user.isGroup(), 'Group', 'User');
+ icon python:test(user.isGroup(), 'img_group', 'img_user');
+ color python:test(user.isGroup(), here.acl_users.group_color, here.acl_users.user_color);
+ ">
+
+ <br />
+
+ <div class="std-text">
+ <img src="" alt="kind" tal:attributes="src icon; alt kind" align="middle">
+ <strong tal:condition="user/isGroup" tal:content="structure string:${user/asHTML} (Group)">toto group management</strong>
+ <strong tal:condition="not: user/isGroup" tal:content="structure string:${user/asHTML} (User)">toto user management</strong>
+ </div>
+
+
+ <h4>Settings</h4>
+
+ <form action="" method="POST" tal:attributes="action here/absolute_url">
+ <tal:block tal:define="
+ label_groups python:user.getGroups();
+ label_groups_no_recurse python:user.getImmediateGroups();
+ label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups);
+ groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse);
+ groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse);
+ roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles());
+ roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles());
+ roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles)
+ ">
+ <ol>
+ <table cellspacing="10">
+ <tr>
+ <!-- User info -->
+ <input type="hidden" name="user" value="" tal:attributes="value user/getUserName">
+ <td valign="top">
+ <table bgcolor="#EEEEEE">
+ <tr>
+ <th class="list-header"><span tal:replace="kind" /> name</th>
+ <td class="list-item">
+ <strong tal:content="structure user/asHTML">
+ </strong>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header">Member of groups</th>
+ <td class="list-item">
+ <span tal:repeat="group groups_no_recurse" >
+ <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header">Implicitly member of groups</th>
+ <td class="list-item">
+ <span tal:repeat="group groups_recurse" >
+ <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header">Has roles</th>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_no_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <th class="list-header">Implicitly has roles (from groups)</th>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="4" align="center"><br>
+ <input type="submit" name="changeUser:method" value="Change" />
+ <tal:block tal:replace="nothing">
+ XXX have to make this work again
+
+ <input type="submit" name="deleteUser:method" value="Delete" />
+ <br>
+ </tal:block>
+ </td>
+ </tr>
+ </table>
+ </td>
+
+ <td valign="middle">
+ =>
+ </td>
+
+ <td valign="top">
+ <table bgcolor="#EEEEEE">
+ <tr>
+ <td>
+ <!-- Groups selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Set groups</th>
+ </tr>
+
+ <tr tal:repeat="group here/getGroups">
+ <td>
+ <input type="checkbox" name="groups:list" value="" checked=""
+ tal:condition="python: group.getUserName() != user.getUserName()"
+ tal:attributes="
+ value group/getUserName;
+ checked python:test(group.getId() in user.getGroupIds(), '1', '')">
+ </td>
+ <td>
+ <div class="list-item" tal:content="structure group/asHTML"></div>
+ </td>
+ </tr>
+ </table>
+
+ <br>
+
+ <!-- Roles selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Set roles</th>
+ </tr>
+
+ <tr tal:repeat="role here/valid_roles">
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <input type="checkbox" name="roles:list" value="" checked=""
+ tal:attributes="value role; checked python:test(role in user.getUserRoles(), '1', '')">
+ </td>
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </ol>
+ </tal:block>
+
+ </form>
+
+
+ <tal:block tal:condition="nothing|user/isGroup">
+ XXX TODO ! XXX
+ <h4>Group contents</h4>
+ <ol>
+ <table bgcolor="#EEEEEE" tal:define="content python:list(user.getImmediateGroups())">
+ <tr class="list-header">
+ <th>Group/User</th>
+ <th class="list-header">Member <br>of groups</th>
+ <th class="list-header">Implicitly <br>member <br>of groups</th>
+ <th class="list-header">Has roles</th>
+ <th class="list-header">Implicitly <br>has roles <br>(from groups)</th>
+ </tr>
+
+ <tr
+ tal:repeat="user python:content" class="" tal:attributes="class python:test(path('repeat/user/odd'), 'row-hilite', 'row-normal')"
+ >
+ <div tal:define="
+ label_groups python:user.getGroups();
+ label_groups_no_recurse python:user.getImmediateGroups();
+ label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups);
+ groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse);
+ groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse);
+ roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles());
+ roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles());
+ roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles);"
+ tal:omit-tag="">
+ <td class="list-item">
+ <span tal:repeat="group groups_no_recurse" >
+ <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ <td class="list-item">
+ <span tal:repeat="group groups_recurse" >
+ <span tal:replace="structure python:user.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_no_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ </div>
+ </tr>
+ </table>
+ </ol>
+ </tal:block>
+
+
+ <h4>Instructions</h4>
+ <ol>
+ <p class="form-help">
+ To change roles for a <span tal:replace="kind" />,
+ select the roles you want to give it and the groups it belongs to in the forms on the right and click "Change".<br />
+ </p>
+ </ol>
+
+ <h4>Important notice / disclaimer</h4>
+
+ <ol>
+ <p class="form-help">
+ This form uses the regular Zope Security API from the underlying user folders. However, you may experience problems with some
+ of them, especially if they are not tuned to allow user adding. For example, an LDAPUserFolder can be configured to disable
+ users management. In case this form doesn't work, you'll have to do things by hand within the 'Users' and 'Groups' GRUF folders.
+ </p>
+
+ <p class="form-help">
+ This is not a GRUF limitation ! :-)
+ </p>
+ </ol>
+
+ </tal:block>
+
+ <h1 tal:replace="structure here/manage_page_footer">Footer</h1>
+
+
--- /dev/null
+ <h1 tal:replace="structure here/manage_page_header">Header</h1>
+ <h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+ tal:replace="structure here/manage_tabs">Tabs</h2>
+
+ <h4>Users sources</h4>
+ <ol>
+ <table cellspacing="10" width="90%" tal:define="groups here/getGroups">
+ <tr tal:repeat="source here/listUserSourceFolders">
+ <th class="list-header">Users source #<span tal:replace="repeat/source/number">1</span></th>
+ <td bgcolor="#EEEEEE" tal:condition="source/isValid"
+ tal:define="meta_type source/acl_users/meta_type|nothing;
+ title_or_id source/acl_users/title|meta_type;">
+ <img src=""
+ tal:attributes="src source/acl_users/icon;
+ title meta_type">
+
+ <a href=""
+ tal:attributes="
+ href string:${source/acl_users/absolute_url}/manage_workspace;
+ title meta_type"
+ tal:content="title_or_id">Title</a>
+ <tal:block condition="not:source/isEnabled">
+ <font color="red"><i>(disabled)</i></font>
+ </tal:block>
+ </td>
+ <td bgcolor="#EEEEEE" tal:condition="not:source/isValid">
+ <font color="red"><strong><i>(invalid or broken)</i></strong></font>
+ </td>
+ </tr>
+ </table>
+ </ol>
+
+ <tal:block
+ tal:condition="not: search_userid"
+ tal:define="global search_userid request/search_userid|nothing"
+ >
+ <tal:block tal:define="global users here/getPureUsers">
+ </tal:block>
+ </tal:block>
+ <tal:block tal:condition="search_userid">
+ <tal:block
+ tal:define="
+ uid search_userid;
+ global users python:[ here.getUser(uid) for uid in here.searchUsersById(uid) if uid ];
+ ">
+ </tal:block>
+ </tal:block>
+
+ <h4>Search</h4>
+ <ol>
+ <div
+ tal:define="have_users python: len(users);">
+ <div class="list-item" tal:condition="python: not have_users and not search_userid">
+ No user available. This happens either if you have no users defined or if
+ the underlying UserFolder cannot retreive the entire users list (for example, LDAPUserFolder
+ is limited in results size).
+ </div>
+ <div class="list-item">
+ Some more users may be available but do not show up there.. This happens if
+ the underlying UserFolder cannot retreive the entire users list (for example,
+ LDAPUserFolder is limited in results size and will return only cached users).
+ </div>
+ <div class="list-item">
+ You can search users giving part of their id with this form.
+ </div>
+ <div>
+ <form action="" tal:attributes="action template/absolute_url">
+ <b>User name:</b>
+ <input name="search_userid" type="text" tal:attributes="value search_userid" />
+ <input type="submit" value="Search" />
+ </form>
+ </div>
+ </div>
+ </ol>
+
+ <h4 tal:condition="not: search_userid">Users management</h4>
+ <h4 tal:condition="search_userid">Search results</h4>
+ <form action="" method="POST" tal:attributes="action request/URL1">
+ <ol>
+ <div tal:condition="python: not users and search_userid">
+ No user found.
+ </div>
+ <table cellspacing="10" width="90%">
+ <tr>
+ <!-- Users selection -->
+ <td valign="top">
+ <table bgcolor="#EEEEEE" width="100%">
+ <tr class="list-header" tal:condition="users">
+ <th> </th>
+ <th>User</th>
+ <th class="list-header">Member <br>of groups</th>
+ <th class="list-header">Implicitly <br>member of*</th>
+ <th class="list-header">Has roles</th>
+ <th class="list-header">Implicitly <br>has roles**</th>
+ </tr>
+
+ <tr
+ tal:repeat="user users"
+ class=""
+ tal:attributes="class python:test(path('repeat/user/odd'), 'row-hilite', 'row-normal')"
+ >
+ <div tal:condition="user"
+ tal:omit-tag=""
+ x:comment="We ignore empty/invalid users"
+ >
+ <div tal:define="
+ label_groups python:user.getGroups();
+ label_groups_no_recurse python:user.getGroups(no_recurse = 1);
+ label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups);
+ groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse);
+ groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse);
+ roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles());
+ roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles());
+ roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles);"
+ tal:omit-tag="">
+ <td><div class="list-item"><input type="checkbox" name="users:list" value="" tal:attributes="value user"></td>
+ <td>
+ <div class="list-item">
+ <img src="img_user" />
+ <strong tal:content="structure user/asHTML">
+ </strong>
+ </td>
+ <td class="list-item">
+ <span tal:repeat="group groups_no_recurse" >
+ <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ <td class="list-item">
+ <span tal:repeat="group groups_recurse" >
+ <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span>
+ </span>
+ </td>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_no_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ <td class="list-item">
+ <div class="list-item">
+ <span tal:repeat="role roles_recurse" >
+ <font color=""
+ tal:attributes="color here/role_color">
+ <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span>
+ </font>
+ </span>
+ </div>
+ </td>
+ </div>
+ </div>
+ </tr>
+ <tr>
+ <td colspan="5">
+ <input type="submit" name="deleteUsers:method" value="Delete" /><br />
+ You can also change group / roles with the form below.
+ </td>
+ </tr>
+ </table>
+
+
+ <div tal:condition="python: not search_userid"
+ tal:define="have_users python: len(users);">
+ <div class="list-item" tal:condition="not: have_users">
+ No user available. This happens either if you have no users defined or if
+ the underlying UserFolder cannot retreive the entire users list (for example, LDAPUserFolder
+ is limited in results size).<br />
+ Use the above search form to search for specific users.
+ </div>
+ </div>
+ </ol>
+
+ <!-- New user -->
+ <h4>User creation</h4>
+ <ol>
+ <table>
+ <tr>
+ <td><div class="list-item"> </div></td>
+ <td>
+ <div class="list-item">Batch user creation list:</div>
+ </td>
+ </tr>
+ <tr>
+ <td><div class="list-item"> </div></td>
+ <td>
+ <div class="list-item">
+ <textarea name="new_users:lines" cols="20" rows="3"></textarea>
+ </div>
+ </td>
+ <td colspan="4">
+ <div class="list-item" valign="top">
+ Newly created users will be affected groups and roles according to the table below.
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td><div class="list-item"> </div></td>
+ <td>
+ <div class="list-item">Default password:</div>
+ </td>
+ </tr>
+ <tr>
+ <td><div class="list-item"> </div></td>
+ <td>
+ <div class="list-item">
+ <input name="default_password:string" size="20" />
+ </div>
+ </td>
+ <td colspan="4">
+ <div class="list-item">
+ Fill in this field to specify a default password for new users,
+ or leave it empty to let GRUF generate random ones.
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" align="center">
+ <input type="submit" name="changeOrCreateUsers:method" value="Create" />
+ </td>
+ </tr>
+ </table>
+ </ol>
+
+
+ <h4>Roles / groups management</h4>
+ <ol>
+ <table>
+ <tr>
+ <td align="center">
+ <div class="list-item">
+ Select one or more users in the upper table, select one or more groups / roles in the table below
+ and click "Change" to affect groups / roles to these users.
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top" align="center" colspan="6">
+ <table bgcolor="#EEEEEE">
+ <tr>
+ <td valign="top">
+ <!-- Groups selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Affect groups</th>
+ </tr>
+
+ <tr tal:repeat="group here/getGroups">
+ <td>
+ <input type="checkbox" name="groups:list" value="" tal:attributes="value group">
+ </td>
+ <td>
+ <div class="list-item" tal:content="structure group/asHTML"></div>
+ </td>
+ </tr>
+
+ <!-- "(None)" item -->
+ <tr>
+ <td><div class="list-item"><input type="checkbox" name="nested_groups:list" value="__None__"></div></td>
+ <td><div class="list-item"><i>(None)</i></div></td>
+ </tr>
+ </table>
+ </td>
+ <td valign="top">
+ <!-- Roles selection -->
+ <table width="100%">
+ <tr class="list-header">
+ <th colspan="2">Affect roles</th>
+ </tr>
+
+ <tr tal:repeat="role here/valid_roles">
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <input type="checkbox" name="roles:list" value="" tal:attributes="value role">
+ </td>
+ <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')">
+ <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div>
+ </td>
+ </tr>
+
+ <!-- "(None)" item -->
+ <tr>
+ <td><div class="list-item"><input type="checkbox" name="roles:list" value="__None__"></div></td>
+ <td><div class="list-item"><i>(None)</i></div></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" align="middle"><input type="submit" name="changeOrCreateUsers:method" value="Change" /></td>
+ </table>
+ </td>
+ </tr>
+ </table>
+
+ <p class="form-help">
+ If you do not select a role, roles won't be reset for the selected users.<br />
+ If you do not select a group, groups won't be reset for the selected users.<br />
+ To explicitly reset groups or roles, just click the "(None)" entry (and no other entry).
+ </p>
+
+ <p class="form-help">
+ * According to the groups inheritance, this group is also recursively member of these groups. <br />This is what we call nested groups.
+ </p>
+ <p class="form-help">
+ ** Accorded to the groups inheritance, this group also has these roles - even if they are not defined explicitly on it.
+ </p>
+
+ </ol>
+ </form>
+
+
+ <h4>Instructions</h4>
+ <ol>
+ <p class="form-help">
+ To change roles for one or several users, select them in the left form,
+ select the roles you want to give them and the groups they belong to in the forms on the right and click "Change".<br />
+ You can also create one or several users by filling the text area (one user per line).
+ The "Change" button will create them with the roles and group affectation you've selected.
+ A random password will be generated for them, and it will be shown in a page so that you can click/paste them somewhere.<br />
+ If you want to kill some users, you can delete them by selecting them and clicking the "Delete" button.
+ </p>
+ </ol>
+
+ <h4>Important notice / disclaimer</h4>
+
+ <ol>
+ <p class="form-help">
+ This form uses the regular Zope Security API from the underlying user folders. However, you may experience problems with some
+ of them, especially if they are not tuned to allow user adding. For example, an LDAPUserFolder can be configured to disable
+ users management. In case this form doesn't work, you'll have to do things by hand within the 'Users' and 'Groups' GRUF folders.
+ </p>
+
+ <p class="form-help">
+ This is not a GRUF limitation ! :-)
+ </p>
+ </ol>
+
+ <h1 tal:replace="structure here/manage_page_footer">Footer</h1>
--- /dev/null
+ <h1 tal:replace="structure here/manage_page_header">Header</h1>
+ <h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+ tal:replace="structure here/manage_tabs">Tabs</h2>
+
+ <h4>The LDAP Wizard section</h4>
+ <ol>
+ <p class="form-help">
+ Here's the place where you can perform a few actions with your LDAP configuration.<br />
+ Of course, if you do not plan to use LDAP with Plone, you can move away from here.<br />
+ First of all, here's a little list of links that you may find useful:
+ </p>
+ <ul>
+ <li><a href="http://ingeniweb.sourceforge.net/Products/GroupUserFolder/doc/README-LDAP.html">The official GRUF+LDAPUserFolder documentation</a> (a must-read !)</li>
+ <li><a href="http://www.dataflake.org/software/ldapuserfolder">The official LDAPUserFolder page</a></li>
+ </ul>
+ </ol>
+
+
+ <tal:block define="
+ have_LDAPUF python: 'LDAPUserFolder' in [ s[0] for s in here.listAvailableUserSources() ];
+ LDAPUF_installed here/hasLDAPUserFolderSource;
+ areLUFGroupsLocal python: LDAPUF_installed and here.areLUFGroupsLocal();
+ ">
+
+
+ <tal:block condition="python: not have_LDAPUF">
+ <h4>LDAPUserFolder status</h4>
+ <ol>
+ <p>
+ Looks like you don't have LDAPUserFolder installed.<br />
+ Please download the latest version from <a href="http://www.dataflake.org/software/ldapuserfolder">The official LDAPUserFolder page</a>.
+ </p>
+ </ol>
+ </tal:block>
+
+ <tal:block condition="python: have_LDAPUF and not LDAPUF_installed">
+ <h4>LDAPUserFolder status</h4>
+ <ol>
+ <p>
+ It seems that you don't have LDAPUserFolder installed or configured as a source for GRUF.<br />
+ Return to the 'sources' tab and add it.
+ </p>
+ </ol>
+ </tal:block>
+
+ <tal:block condition="python: have_LDAPUF and LDAPUF_installed">
+ <h4>Groups status</h4>
+ <ol>
+ <tal:block condition="areLUFGroupsLocal">
+ Your groups are reported to be stored in ZODB.<br />
+ You can create groups with <a href="manage_groups">this link</a>.
+ Once you've created groups, don't forget to come back here and see the 'update mapping' section below.<br />
+ <tal:block condition="here/haveLDAPGroupFolder">
+
+ <font color="red">
+ <dl>
+ <dt><b>WARNING</b></dt>
+ <dd>It seems that your groups source is LDAPGroupFolder.<br />
+ This is not recommanded since this groups source is only for managing groups when
+ they are stored on your LDAP Server. Please go back to the sources tab and change it.<br />
+ A regular UserFolder instead should do it.
+ </dd>
+ </dl>
+ </font>
+
+ </tal:block>
+ </tal:block>
+ <tal:block condition="not: areLUFGroupsLocal">
+ Your groups are reported to be stored in your LDAP database.
+ </tal:block>
+ </ol>
+
+ <h4>Groups mapping</h4>
+ <ol>
+ <p class="form-help">
+ To manage groups with a LDAPUserFolder, one must <b>map</b> LDAP groups to Zope Roles.<br />
+ You can do this mapping manually or click this button to have it done automatically.<br />
+ Please not that any previously existing ldap-group - to - zope-role mapping may be lost.
+ </p>
+
+ <tal:block condition="here/getInvalidMappings">
+ <p class="form-help">
+ <strong>You must do this even if your groups are not stored on your LDAP database</strong>
+ </p>
+ <p class="form-help">
+ To help you in this task, you can have a look at the following table, which summs up<br />
+ the mappings done (or not done!) in LDAPUserFolder.
+ </p>
+
+ <font color="red">
+ <dl>
+ <dt><b>WARNING</b></dt>
+ <dd>Your mapping doesn't look good... You surely need to click the 'update mapping' button.<br />
+ </dd>
+ </dl>
+ </font>
+ </tal:block>
+
+ <tal:block condition="not: here/getInvalidMappings">
+ Your mapping looks good. It's not necessary to update it.
+ </tal:block>
+
+ <table bgcolor="#FFFFFF">
+ <thead>
+ <th class="list-header">LDAP group</th>
+ <th class="list-header">is mapped to</th>
+ <th class="list-header">GRUF group</th>
+ </thead>
+ <tbody>
+ <tr tal:repeat="group_info here/listLDAPUserFolderMapping">
+ <td bgcolor="#EEEEEE" tal:content="python:group_info[1]"></td>
+ <td align="center" bgcolor="#EEEEEE">
+ =>
+ </td>
+ <td bgcolor="#EEEEEE" tal:content="python:group_info[0]"></td>
+ </tr>
+ </tbody>
+ </table>
+ <form action="updateLDAPUserFolderMapping">
+ <input type="submit" value="Update LDAP mapping" />
+ </form>
+ </ol>
+ </tal:block>
+
+ </tal:block>
+
+ <h1 tal:replace="structure here/manage_page_footer">Footer</h1>
--- /dev/null
+<dtml-comment> -*- mode: dtml; dtml-top-element: "body" -*- </dtml-comment>
+<dtml-var manage_page_header>
+
+<dtml-var "manage_form_title(this(), _,
+ form_title='Add LDAP Group Folder',
+ )">
+
+<p class="form-help">
+ Add a new LDAPGroupFolder with this form.
+</p>
+
+<form action="manage_addLDAPGroupFolder" method="POST">
+ <table cellspacing="0" cellpadding="3">
+
+ <tr>
+ <td align="left" valign="TOP"><div class="form-optional">
+ Title
+ </div></td>
+ <td align="left" valign="TOP"><div class="form-element">
+ <input type="text" name="title" size="40" />
+ </div></td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="TOP"><div class="form-label">LDAP User Folder</div></td>
+ <td align="left" valign="TOP"><div class="form-element">
+ <select name="luf">
+ <dtml-in "aq_parent.listUserSourceFolders()">
+ <dtml-with getUserFolder>
+ <dtml-if expr="meta_type=='LDAPUserFolder'">
+ <dtml-let luf_path="_.string.join( getPhysicalPath(), '/' )">
+ <dtml-let parentfolderid="aq_parent.id">
+ <option value="&dtml-parentfolderid;">&dtml-luf_path; (&dtml-meta_type;)</option>
+ </dtml-let>
+ </dtml-let>
+ </dtml-if>
+ </dtml-with>
+ </dtml-in>
+
+ </div></td>
+ </tr>
+
+ <tr>
+ <td> </td>
+ <td>
+ <br />
+ <input type="SUBMIT" value=" Add ">
+ </td>
+ </tr>
+
+ </table>
+</form>
+
+<dtml-var manage_page_footer>
+
--- /dev/null
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<h2>Control Creation of Group Workspaces</h2>
+<p>
+ If "workspace creation" is on, workspaces will be automatically created (if they do not exist)
+ for groups upon creation.
+</p>
+<form action="toggleGroupWorkspacesCreation" method="post">
+ <p>Workspaces creation is <strong><dtml-var "getGroupWorkspacesCreationFlag() and 'on' or 'off'"></strong></p>
+ <input type="submit" value="Turn Workspace Creation <dtml-var "getGroupWorkspacesCreationFlag() and 'off' or 'on'">" />
+</form>
+
+<h2>Set Workspaces Folder Name</h2>
+<p>
+ Provides the name of the folder or object manager that will contain all group workspaces.
+ It will be created if it does not exist, and must be in the same container as the groups tool.
+ (If you really need a path here, contact the developers.)
+</p>
+<p>
+ The default is <em>GroupWorkspaces</em>.
+</p>
+<form action="manage_setGroupWorkspacesFolder" method="post">
+ <p><strong>Workspace container id</strong> <input type="text" name="id" value="&dtml-getGroupWorkspacesFolderId;" /></p>
+ <input type="submit" value="Change" />
+</form>
+
+
+<h2>Set Group Workspaces Container Type</h2>
+<p>
+ Provide the name of the Type that will be created when creating the first Group Workspace.
+ This object will be at the root of your Plone site, with the id "GroupWorkspaces".
+</p>
+<form action="manage_setGroupWorkspaceContainerType" method="post">
+ <p><strong>Create worspaces container as type</strong> <input type="text" name="type" value="&dtml-getGroupWorkspaceContainerType;" /></p>
+ <input type="submit" value="Change" />
+</form>
+
+
+<h2>Set Group Workspaces Type</h2>
+<p>
+ Provide the name of the Type that will be created to serve as the Group Workspaces. You may use
+ <code>Folder</code>, which is present by default, <code>GroupSpace</code>, which comes
+ with GRUF, or a type of you own definition. See the portal_types tool for types.
+</p>
+<form action="manage_setGroupWorkspaceType" method="post">
+ <p><strong>Create workspaces as type</strong> <input type="text" name="type" value="&dtml-getGroupWorkspaceType;" /></p>
+ <input type="submit" value="Change" />
+</form>
+
+
+<dtml-var manage_page_footer>
--- /dev/null
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<h3> <code>portal_groupdata</code> Tool </h3>
+
+<p> This tool is responsible for handling the storage of properties on
+user groups.
+</p>
+
+<dtml-var manage_page_footer>
--- /dev/null
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<h3> <code>portal_groups</code> Tool </h3>
+
+<p> This tool provides user-group management functions for use in a
+CMF site. Its interface provides a common front-end to various group
+implementations.
+</p>
+
+<dtml-var manage_page_footer>
--- /dev/null
+<dtml-var manage_page_header>
+
+<dtml-with "_(management_view='Groups')">
+ <dtml-var manage_tabs>
+</dtml-with>
+
+<p class="form-help">
+ This view shows all available groups at the specified branch
+ and allows deletion and addition.
+</p>
+
+<dtml-in expr="getGroups()">
+
+ <dtml-if name="sequence-start">
+ <form action="&dtml-URL1;" method="post">
+ <table border="0" cellpadding="2" cellspacing="0" width="95%">
+ <tr class="list-header">
+ <td align="left" valign="top" width="16"> </td>
+ <td><div class="form-label"> Friendly Name </div></td>
+ <td><div class="form-label"> Object Class </div></td>
+ <td><div class="form-label"> Distinguished Name </div></td>
+ </tr>
+ </dtml-if>
+
+ <dtml-if sequence-odd>
+ <tr class="row-normal">
+ <dtml-else>
+ <tr class="row-hilite">
+ </dtml-if>
+ <td align="left" valign="top" width="16">
+ <input type="checkbox" name="dns:list" value="&dtml-sequence-item;" />
+ </td>
+ <td><div class="form-text">
+ <dtml-var name="sequence-key">
+ </div></td>
+ <td><div class="form-text">
+ <dtml-var expr="getGroupType( _['sequence-item'] )">
+ </div></td>
+ <td><div class="form-text">
+ <dtml-var name="sequence-item" size="60" etc="...">
+ </div></td>
+ </tr>
+
+ <dtml-if name="sequence-end">
+ <tr>
+ <td align="left" valign="top" width="16"> </td>
+ <td align="left" valign="top" colspan="2"><div class="form-element">
+ <input class="form-element" type="submit"
+ name="manage_deleteGroups:method"
+ value="Delete" />
+ </div></td>
+ </tr>
+ </table>
+ </form>
+ </dtml-if>
+
+<dtml-else>
+ <br />
+ <div class="form-label">
+ No groups found.
+ Please check the settings "Group base DN" and "Groups search scope"
+ and make sure your LDAP tree contains suitable group records.
+ </div>
+
+</dtml-in>
+
+<p><br></p>
+
+<form action="manage_addGroup" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="95%">
+
+ <tr class="section-bar">
+ <td colspan="2" align="left" valign="top"><div class="form-label">
+ Add Group
+ </div></td>
+ </tr>
+
+ <tr>
+ <td colspan="2" align="left" valign="top"><div class="form-text">
+ Add a new group on this LDAP branch by specifying a group name
+ and hitting "Add".
+ The name is a "friendly" name, meaning it
+ is not a dn or does not contain any LDAP-sepecific elements.
+ </div></td>
+ </tr>
+
+ <tr><td colspan="2"> </td></tr><tr>
+ <td align="left" valign="absmiddle"><div class="form-label">
+ Group Name
+ </div></td>
+ <td align="LEFT" valign="TOP">
+ <input type="TEXT" name="newgroup_name" size="50"
+ value="MyGroup" />
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="absmiddle"><div class="form-label">
+ Group object class
+ </div></td>
+ <td align="LEFT" valign="TOP">
+ <select name="newgroup_type">
+ <option value="groupOfUniqueNames"> groupOfUniqueNames </option>
+ <option value="groupOfNames"> groupOfNames </option>
+ <option value="accessGroup"> accessGroup </option>
+ <option value="group"> group </option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" colspan="2">
+ <input class="form-element" type="SUBMIT" value=" Add " />
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<p><hr></p>
+
+<table cellspacing="0" cellpadding="2" width="95%">
+ <tr>
+ <td align="left" valign="top"><div class="form-text">
+ This section determines if LDAP groups are mapped to Zope roles
+ and what they map to.
+ </div></td>
+ </tr>
+</table>
+
+<br />
+
+<dtml-in getGroupMappings>
+
+ <dtml-if name="sequence-start">
+ <form action="&dtml-URL1;" method="post">
+ <table border="0" cellpadding="2" cellspacing="0" width="95%">
+ <tr class="list-header">
+ <td align="left" valign="top" width="16"> </td>
+ <td><div class="form-label"> LDAP Group </div></td>
+ <td><div class="form-label"> Zope Role </div></td>
+ </tr>
+ </dtml-if>
+
+ <dtml-if sequence-odd>
+ <tr class="row-normal">
+ <dtml-else>
+ <tr class="row-hilite">
+ </dtml-if>
+ <td align="left" valign="top" width="16">
+ <input type="checkbox" name="group_names:list" value="&dtml-sequence-key;" />
+ </td>
+ <td><div class="form-text"> &dtml-sequence-key; </div></td>
+ <td><div class="form-text"> &dtml-sequence-item; </div></td>
+ </tr>
+
+ <dtml-if name="sequence-end">
+ <tr>
+ <td align="left" valign="top" width="16"> </td>
+ <td align="left" valign="top" colspan="2"><div class="form-element">
+ <input class="form-element" type="submit"
+ name="manage_deleteGroupMappings:method"
+ value="Delete" />
+ </div></td>
+ </tr>
+ </table>
+ </dtml-if>
+
+<dtml-else>
+ <p>(No group mappings specified at this time.)</p>
+
+</dtml-in>
+
+<p> </p>
+
+<form action="&dtml-URL1;" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="95%">
+
+ <tr class="section-bar">
+ <td colspan="4" align="left" valign="top"><div class="form-label">
+ Add LDAP group to Zope role mapping
+ </div></td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="absmiddle"><div class="form-label">
+ Map this LDAP Group...
+ </div></td>
+ <td align="LEFT" valign="TOP">
+ <select name="group_name">
+ <dtml-in getGroups sort>
+ <option>&dtml-sequence-key;</option>
+ </dtml-in>
+ </select>
+ </td>
+ <td align="left" valign="absmiddle"><div class="form-label">
+ ... to this Zope Role
+ </div></td>
+ <td align="LEFT" valign="TOP">
+ <select name="role_name">
+ <dtml-in expr="_.reorder( valid_roles()
+ , without=( 'Anonymous', 'Authenticated', 'Owner' )
+ )" sort>
+ <option>&dtml-sequence-item;</option>
+ </dtml-in>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" colspan="4">
+ <input class="form-element" type="SUBMIT" value=" Add "
+ name="manage_addGroupMapping:method">
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<dtml-var manage_page_footer>
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: global_symbols.py 32384 2006-10-27 10:00:55Z encolpe $
+__docformat__ = 'restructuredtext'
+
+import os
+import string
+
+# Check if we have to be in debug mode
+import Log
+if os.path.isfile(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'debug.txt')):
+ Log.LOG_LEVEL = Log.LOG_DEBUG
+ DEBUG_MODE = 1
+else:
+ Log.LOG_LEVEL = Log.LOG_NOTICE
+ DEBUG_MODE = 0
+
+from Log import *
+
+# Retreive version
+if os.path.isfile(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'version.txt')):
+ __version_file_ = open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'version.txt'), 'r', )
+ version__ = __version_file_.read()[:-1]
+ __version_file_.close()
+else:
+ version__ = "(UNKNOWN)"
+
+# Check if we are in preview mode
+PREVIEW_PLONE21_IN_PLONE20_ = 0
+splitdir = os.path.split(os.path.abspath(os.path.dirname(__file__)))
+products = os.path.join(*splitdir[:-1])
+version_file = os.path.join(products, 'CMFPlone', 'version.txt')
+if os.path.isfile(version_file):
+ # We check if we have Plone 2.0
+ f = open(version_file, "r")
+ v = f.read()
+ f.close()
+ if string.find(v, "2.0.") != -1:
+ PREVIEW_PLONE21_IN_PLONE20_ = 1
+
+
+# Group prefix
+GROUP_PREFIX = "group_"
+GROUP_PREFIX_LEN = len(GROUP_PREFIX)
+
+# Batching range for ZMI pages
+MAX_USERS_PER_PAGE = 100
+
+# Max allowrd users or groups to enable tree view
+MAX_TREE_USERS_AND_GROUPS = 100
+
+# Users/groups tree cache time (in seconds)
+# This is used in management screens only
+TREE_CACHE_TIME = 10
+
+# List of user names that are likely not to be valid user names.
+# This list is for performance reasons in ZMI views. If some actual user names
+# are inside this list, management screens won't work for them but they
+# will still be able to authenticate.
+INVALID_USER_NAMES = [
+ 'BASEPATH1', 'BASEPATH2', 'BASEPATH3', 'a_', 'URL', 'acl_users', 'misc_',
+ 'management_view', 'management_page_charset', 'REQUEST', 'RESPONSE',
+ 'MANAGE_TABS_NO_BANNER', 'tree-item-url', 'SCRIPT_NAME', 'n_', 'help_topic',
+ 'Zope-Version', 'target',
+ ]
+
+# LDAPUserFolder-specific stuff
+LDAPUF_METHOD = "manage_addLDAPSchemaItem" # sample method to determine if a uf is an ldapuf
+LDAP_GROUP_RDN = "cn" # rdn attribute for groups
+
+LOCALROLE_BLOCK_PROPERTY = "__ac_local_roles_block__" # Property used for lr blocking
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+VOCABULARY:
+
+ - [Pure] User: A user is a user atom who can log itself on, and
+ have additional properties such as domains and password.
+
+ - Group: A group is a user atom other atoms can belong to.
+
+ - User atom: Abstract representation of either a User or
+ a Group.
+
+ - Member (of a group): User atom inside a group.
+
+ - Name (of an atom): For a user, the name can be set by
+ the underlying user folder but usually id == name.
+ For a group, its id is prefixed, but its name is NOT prefixed by 'group_'.
+ For method taking a name instead of an id (eg. getUserByName()),
+ if a user and a group have the same name,
+ the USER will have precedence over the group.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: IUserFolder.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+from Interface import Attribute
+try:
+ from Interface import Interface
+except ImportError:
+ # for Zope versions before 2.6.0
+ from Interface import Base as Interface
+
+
+
+class IUserFolder(Interface):
+
+ # #
+ # Regular Zope UserFolder API #
+ # #
+
+ # User atom access
+
+ def getUserNames():
+ """
+ Return a list of all possible user atom names in the system.
+ Groups will be returned WITHOUT their prefix by this method.
+ So, there might be a collision between a user name and a group name.
+ [NOTA: This method is time-expensive !]
+ """
+
+ def getUserIds():
+ """
+ Return a list of all possible user atom ids in the system.
+ WARNING: Please see the id Vs. name consideration at the
+ top of this document. So, groups will be returned
+ WITH their prefix by this method
+ [NOTA: This method is time-expensive !]
+ """
+
+ def getUser(name):
+ """Return the named user atom object or None
+ NOTA: If no user can be found, we try to append a group prefix
+ and fetch the user again before returning 'None'. This will ensure
+ backward compatibility. So in fact, both group id and group name can be
+ specified to this method.
+ """
+
+ def getUsers():
+ """Return a list of user atom objects in the users cache.
+ In case of some UF implementations, the returned object may only be a subset
+ of all possible users.
+ In other words, you CANNOT assert that len(getUsers()) equals len(getUserNames()).
+ With cache-support UserFolders, such as LDAPUserFolder, the getUser() method will
+ return only cached user objects instead of fetching all possible users.
+ So this method won't be very time-expensive, but won't be accurate !
+ """
+
+ def getUserById(id, default):
+ """Return the user atom corresponding to the given id.
+ If default is provided, return default if no user found, else return None.
+ """
+
+ def getUserByName(name, default):
+ """Same as getUserById() but works with a name instead of an id.
+ If default is provided, return default if no user found, else return None.
+ [NOTA: Theorically, the id is a handle, while the name is the actual login name.
+ But difference between a user id and a user name is unsignificant in
+ all current User Folder implementations... except for GROUPS.]
+ """
+
+ def hasUsers():
+ """
+ From Zope 2.7's User.py:
+ This is not a formal API method: it is used only to provide
+ a way for the quickstart page to determine if the default user
+ folder contains any users to provide instructions on how to
+ add a user for newbies. Using getUserNames or getUsers would have
+ posed a denial of service risk.
+ In GRUF, this method always return 1."""
+
+
+ # Search interface for users; they won't return groups in any case.
+
+ def searchUsersByName(search_term):
+ """Return user ids which match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying user folder:
+ it may return all users, return only cached users (for LDAPUF) or return no users.
+ """
+
+ def searchUsersById(search_term):
+ """Return users whose id match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying user folder:
+ it may return all users, return only cached users (for LDAPUF) or return no users.
+ """
+
+ def searchUsersByAttribute(attribute, search_term):
+ """Return user ids whose 'attribute' match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying user folder:
+ it may return all users, return only cached users (for LDAPUF) or return no users.
+ This will return all users whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON USER FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF).
+ 'attribute' can be 'id' or 'name' for all UF kinds, or anything else for LDAPUF.
+ [NOTA: This method is time-expensive !]
+ """
+
+ # Search interface for groups;
+
+ def searchGroupsByName(search_term):
+ """Return group ids which match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying group folder:
+ it may return all groups, return only cached groups (for LDAPUF) or return no groups.
+ """
+
+ def searchGroupsById(search_term):
+ """Return groups whose id match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying group folder:
+ it may return all groups, return only cached groups (for LDAPUF) or return no groups.
+ """
+
+ def searchGroupsByAttribute(attribute, search_term):
+ """Return group ids whose 'attribute' match the specified search_term.
+ If search_term is an empty string, behaviour depends on the underlying group folder:
+ it may return all groups, return only cached groups (for LDAPUF) or return no groups.
+ This will return all groups whose name contains search_term (whaterver its case).
+ THIS METHOD MAY BE VERY EXPENSIVE ON GROUP FOLDER KINDS WHICH DO NOT PROVIDE A
+ SEARCHING METHOD (ie. every UF kind except LDAPUF).
+ 'attribute' can be 'id' or 'name' for all UF kinds, or anything else for LDAPUF.
+ [NOTA: This method is time-expensive !]
+ """
+
+
+ # User access
+
+ def getPureUserNames():
+ """Same as getUserNames() but without groups
+ """
+
+ def getPureUserIds():
+ """Same as getUserIds() but without groups
+ """
+
+ def getPureUsers():
+ """Same as getUsers() but without groups.
+ """
+
+ def getPureUser(id):
+ """Same as getUser() but forces returning a user and not a group
+ """
+
+ # Group access
+
+ def getGroupNames():
+ """Same as getUserNames() but without pure users.
+ """
+
+ def getGroupIds():
+ """Same as getUserIds() but without pure users.
+ """
+
+ def getGroups():
+ """Same as getUsers() but without pure users.
+ In case of some UF implementations, the returned object may only be a subset
+ of all possible users.
+ In other words, you CANNOT assert that len(getUsers()) equals len(getUserNames()).
+ With cache-support UserFolders, such as LDAPUserFolder, the getUser() method will
+ return only cached user objects instead of fetching all possible users.
+ So this method won't be very time-expensive, but won't be accurate !
+ """
+
+ def getGroup(name):
+ """Return the named group object or None. As usual, 'id' is prefixed.
+ """
+
+ def getGroupById(id):
+ """Same as getUserById(id) but forces returning a group.
+ """
+
+ def getGroupByName(name):
+ """Same as getUserByName(name) but forces returning a group.
+ The specified name MUST NOT be prefixed !
+ """
+
+
+ # Mutators
+
+ def userFolderAddUser(name, password, roles, domains, groups, **kw):
+ """API method for creating a new user object. Note that not all
+ user folder implementations support dynamic creation of user
+ objects.
+ Groups can be specified by name or by id (preferabily by name)."""
+
+ def userFolderEditUser(name, password, roles, domains, groups, **kw):
+ """API method for changing user object attributes. Note that not
+ all user folder implementations support changing of user object
+ attributes.
+ Groups can be specified by name or by id (preferabily by name)."""
+
+ def userFolderUpdateUser(name, password, roles, domains, groups, **kw):
+ """Same as userFolderEditUser, but with all arguments except name
+ being optional.
+ """
+
+ def userFolderDelUsers(names):
+ """API method for deleting one or more user atom objects. Note that not
+ all user folder implementations support deletion of user objects."""
+
+ def userFolderAddGroup(name, roles, groups, **kw):
+ """API method for creating a new group.
+ """
+
+ def userFolderEditGroup(name, roles, groups, **kw):
+ """API method for changing group object attributes.
+ """
+
+ def userFolderUpdateGroup(name, roles, groups, **kw):
+ """Same as userFolderEditGroup but with all arguments (except name) being
+ optinal.
+ """
+
+ def userFolderDelGroups(names):
+ """API method for deleting one or more group objects.
+ Implem. note : All ids must be prefixed with 'group_',
+ so this method ends up beeing only a filter of non-prefixed ids
+ before calling userFolderDelUsers().
+ """
+
+ # User mutation
+
+
+ # XXX do we have to allow a user to be renamed ?
+## def setUserId(id, newId):
+## """Change id of a user atom. The user name might be changed as well by this operation.
+## """
+
+## def setUserName(id, newName):
+## """Change the name of a user atom. The user id might be changed as well by this operation.
+## """
+
+ def userSetRoles(id, roles):
+ """Change the roles of a user atom
+ """
+
+ def userAddRole(id, role):
+ """Append a role for a user atom
+ """
+
+ def userRemoveRole(id, role):
+ """Remove the role of a user atom.
+ This will not, of course, affect implicitly-acquired roles from the user groups.
+ """
+
+ def userSetPassword(id, newPassword):
+ """Set the password of a user
+ """
+
+ def userSetDomains(id, domains):
+ """Set domains for a user
+ """
+
+ def userGetDomains(id, ):
+ """Get domains for a user
+ """
+
+ def userAddDomain(id, domain):
+ """Append a domain to a user
+ """
+
+ def userRemoveDomain(id, domain):
+ """Remove a domain from a user
+ """
+
+ def userSetGroups(userid, groupnames):
+ """Set the groups of a user. Groupnames are, as usual, not prefixed.
+ However, a groupid can be given as a fallback
+ """
+
+ def userAddGroup(id, groupname):
+ """add a group to a user atom. Groupnames are, as usual, not prefixed.
+ However, a groupid can be given as a fallback
+ """
+
+ def userRemoveGroup(id, groupname):
+ """remove a group from a user atom. Groupnames are, as usual, not prefixed.
+ However, a groupid can be given as a fallback
+ """
+
+
+ # Security management
+
+ def setRolesOnUsers(roles, userids):
+ """Set a common set of roles for a bunch of user atoms.
+ """
+
+## def setUsersOfRole(usernames, role):
+## """Sets the users of a role.
+## XXX THIS METHOD SEEMS TO BE SEAMLESS.
+## """
+
+ def getUsersOfRole(role, object = None):
+ """Gets the user (and group) ids having the specified role...
+ ...on the specified Zope object if it's not None
+ ...on their own information if the object is None.
+ NOTA: THIS METHOD IS VERY EXPENSIVE.
+ """
+
+ def getRolesOfUser(userid):
+ """Alias for user.getRoles()
+ """
+
+ def userFolderAddRole(role):
+ """Add a new role. The role will be appended, in fact, in GRUF's surrounding folder.
+ """
+
+ def userFolderDelRoles(roles):
+ """Delete roles.
+ The removed roles will be removed from the UserFolder's users and groups as well,
+ so this method can be very time consuming with a large number of users.
+ """
+
+ def userFolderGetRoles():
+ """List the roles defined at the top of GRUF's folder.
+ """
+
+
+ # Groups support
+ def setMembers(groupid, userids):
+ """Set the members of the group
+ """
+
+ def addMember(groupid, id):
+ """Add a member to a group
+ """
+
+ def removeMember(groupid, id):
+ """Remove a member from a group
+ """
+
+ def hasMember(groupid, id):
+ """Return true if the specified atom id is in the group.
+ This is the contrary of IUserAtom.isInGroup(groupid).
+ THIS CAN BE VERY EXPENSIVE"""
+
+ def getMemberIds(groupid):
+ """Return the list of member ids (groups and users) in this group.
+ It will unmangle nested groups as well.
+ THIS METHOD CAN BE VERY EXPENSIVE AS IT NEEDS TO FETCH ALL USERS.
+ """
+
+ def getUserMemberIds(groupid):
+ """Same as listMemberIds but only return user ids
+ THIS METHOD CAN BE VERY EXPENSIVE AS IT NEEDS TO FETCH ALL USERS.
+ """
+
+ def getGroupMemberIds(groupid):
+ """Same as listMemberUserIds but only return group ids.
+ THIS METHOD CAN BE VERY EXPENSIVE AS IT NEEDS TO FETCH ALL USERS.
+ """
+
+
+ # Local roles acquisition blocking support
+ def acquireLocalRoles(folder, status):
+ """Enable or disable local role acquisition on the specified folder.
+ If status is true, it will enable, else it will disable.
+ """
+
+ def isLocalRoleAcquired(folder):
+ """Return true if the specified folder allows local role acquisition.
+ """
+
+ # Audit & security checking methods
+
+ def getAllLocalRoles(object):
+ """getAllLocalRoles(self, object): return a dictionnary {user: roles} of local
+ roles defined AND herited at a certain point. This will handle lr-blocking
+ as well.
+ """
+
+
+class IUserAtom(Interface):
+ """
+ This interface is an abstract representation of what both a User and a Group can do.
+ """
+ # Accessors
+
+ def getId(unprefixed = 0):
+ """Get the ID of the user. The ID can be used, at least from
+ Python, to get the user from the user's UserDatabase.
+ If unprefixed, remove all prefixes in any case."""
+
+ def getUserName():
+ """Alias for getName()
+ """
+
+ def getName():
+ """Get user's or group's name.
+ For a user, the name can be set by the underlying user folder but usually id == name.
+ For a group, the ID is prefixed, but the NAME is NOT prefixed by 'group_'.
+ """
+
+ def getRoles():
+ """Return the list of roles assigned to a user atom.
+ This will never return gruf-related roles.
+ """
+
+ # Properties are defined depending on the underlying user folder: some support
+ # properties mutation (such as LDAPUserFolder), some do not (such as regular UF).
+
+ def getProperty(name):
+ """Get a property's value.
+ Will raise if not available.
+ """
+
+ def hasProperty(name):
+ """Return true if the underlying user object has a value for the property.
+ """
+
+ # Mutators
+
+ def setProperty(name, value):
+ """Set a property's value.
+ As some user folders cannot set properties, this method is not guaranteed to work
+ and will raise a NotImplementedError if the underlying user folder cannot store
+ properties (or _this_ particular property) for a user.
+ """
+
+ # XXX We do not allow user name / id changes
+## def setId(newId):
+## """Set the id of the user or group. This might change its name as well.
+## """
+
+## def setName(newName):
+## """Set the name of the user or group. Depending on the UserFolder implementation,
+## this might change the id as well.
+## """
+
+ def setRoles(roles):
+ """Change user's roles
+ """
+
+ def addRole(role):
+ """Append a role to the user
+ """
+
+ def removeRole(role):
+ """Remove a role from the user's ones
+ """
+
+ # Security-related methods
+
+ def getRolesInContext(object):
+ """Return the list of roles assigned to the user,
+ including local roles assigned in context of
+ the passed in object."""
+
+ def has_permission(permission, object):
+ """Check to see if a user has a given permission on an object."""
+
+ def allowed(object, object_roles=None):
+ """Check whether the user has access to object. The user must
+ have one of the roles in object_roles to allow access."""
+
+ def has_role(roles, object=None):
+ """Check to see if a user has a given role or roles."""
+
+
+
+ # Group management
+
+ # XXX TODO: CLARIFY ID VS. NAME
+
+ def isGroup():
+ """Return true if this atom is a group.
+ """
+
+ def getGroupNames():
+ """Return the names of the groups that the user or group is directly a member of.
+ Return an empty list if the user or group doesn't belong to any group.
+ Doesn't include transitive groups."""
+
+ def getGroupIds():
+ """Return the names of the groups that the user or group is a member of.
+ Return an empty list if the user or group doesn't belong to any group.
+ Doesn't include transitive groups."""
+
+ def getGroups():
+ """getAllGroupIds() alias.
+ Return the IDS (not names) of the groups that the user or group is a member of.
+ Return an empty list if the user or group doesn't belong to any group.
+ THIS WILL INCLUDE TRANSITIVE GROUPS AS WELL."""
+
+ def getAllGroupIds():
+ """Return the names of the groups that the user or group is a member of.
+ Return an empty list if the user or group doesn't belong to any group.
+ Include transitive groups."""
+
+ def getAllGroupNames():
+ """Return the names of the groups that the user or group is directly a member of.
+ Return an empty list if the user or group doesn't belong to any group.
+ Include transitive groups."""
+
+ def isInGroup(groupid):
+ """Return true if the user is member of the specified group id
+ (including transitive groups)"""
+
+ def setGroups(groupids):
+ """Set 'groupids' groups for the user or group.
+ """
+
+ def addGroup(groupid):
+ """Append a group to the current object's groups.
+ """
+
+ def removeGroup(groupid):
+ """Remove a group from the object's groups
+ """
+
+ def getRealId():
+ """Return group id WITHOUT group prefix.
+ For a user, return regular user id.
+ This method is essentially internal.
+ """
+
+
+class IUser(IUserAtom):
+ """
+ A user is a user atom who can log itself on, and
+ have additional properties such as domains and password.
+ """
+
+ # Accessors
+
+ def getDomains():
+ """Return the list of domain restrictions for a user"""
+
+ # Mutators
+
+ def setPassword(newPassword):
+ """Set user's password
+ """
+
+ def setDomains(domains):
+ """Replace domains for the user
+ """
+
+ def addDomain(domain):
+ """Append a domain for the user
+ """
+
+ def removeDomain(domain):
+ """Remove a domain for the user
+ """
+
+
+class IGroup(Interface):
+ """
+ A group is a user atom other atoms can belong to.
+ """
+ def getMemberIds(transitive = 1, ):
+ """Return the member ids (users and groups) of the atoms of this group.
+ This method can be very expensive !"""
+
+ def getUserMemberIds(transitive = 1, ):
+ """Return the member ids (users only) of the users of this group"""
+
+ def getGroupMemberIds(transitive = 1, ):
+ """Return the members ids (groups only) of the groups of this group"""
+
+ def hasMember(id):
+ """Return true if the specified atom id is in the group.
+ This is the contrary of IUserAtom.isInGroup(groupid)"""
+
+ def addMember(userid):
+ """Add a user the the current group"""
+
+ def removeMember(userid):
+ """Remove a user from the current group"""
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: __init__.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+# interface definitions for use by Plone
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+## Copyright (c) 2003 The Connexions Project, All Rights Reserved
+## initially written by J Cameron Cooper, 11 June 2003
+## concept with Brent Hendricks, George Runyan
+"""Groups tool interface
+
+Goes along the lines of portal_memberdata, but for groups.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: portal_groupdata.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+from Interface import Attribute
+try:
+ from Interface import Interface
+except ImportError:
+ # for Zope versions before 2.6.0
+ from Interface import Base as Interface
+
+class portal_groupdata(Interface):
+ """ A helper tool for portal_groups that transparently adds
+ properties to groups and provides convenience methods"""
+
+## id = Attribute('id', "Must be set to 'portal_groupdata'")
+
+ def wrapGroup(g):
+ """ Returns an object implementing the GroupData interface"""
+
+
+class GroupData(Interface):
+ """ An abstract interface for accessing properties on a group object"""
+
+ def setProperties(properties=None, **kw):
+ """Allows setting of group properties en masse.
+ Properties can be given either as a dict or a keyword parameters list"""
+
+ def getProperty(id):
+ """ Return the value of the property specified by 'id' """
+
+ def getProperties():
+ """ Return the properties of this group. Properties are as usual in Zope."""
+
+ def getGroupId():
+ """ Return the string id of this group, WITHOUT group prefix."""
+
+ def getMemberId():
+ """This exists only for a basic user/group API compatibility
+ """
+
+ def getGroupName():
+ """ Return the name of the group."""
+
+ def getGroupMembers():
+ """ Return a list of the portal_memberdata-ish members of the group."""
+
+ def getAllGroupMembers():
+ """ Return a list of the portal_memberdata-ish members of the group
+ including transitive ones (ie. users or groups of a group in that group)."""
+
+ def getGroupMemberIds():
+ """ Return a list of the user ids of the group."""
+
+ def getAllGroupMemberIds():
+ """ Return a list of the user ids of the group.
+ including transitive ones (ie. users or groups of a group in that group)."""
+
+ def addMember(id):
+ """ Add the existing member with the given id to the group"""
+
+ def removeMember(id):
+ """ Remove the member with the provided id from the group """
+
+ def getGroup():
+ """ Returns the actual group implementation. Varies by group
+ implementation (GRUF/Nux/et al)."""
--- /dev/null
+# -*- coding: utf-8 -*-
+## GroupUserFolder
+## Copyright (C)2006 Ingeniweb
+
+## 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; see the file COPYING. If not, write to the
+## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+## Copyright (c) 2003 The Connexions Project, All Rights Reserved
+## initially written by J Cameron Cooper, 11 June 2003
+## concept with Brent Hendricks, George Runyan
+"""Groups tool interface
+
+Goes along the lines of portal_membership, but for groups.
+"""
+__version__ = "$Revision: $"
+# $Source: $
+# $Id: portal_groups.py 30098 2006-09-08 12:35:01Z encolpe $
+__docformat__ = 'restructuredtext'
+
+
+from Interface import Attribute
+try:
+ from Interface import Interface
+except ImportError:
+ # for Zope versions before 2.6.0
+ from Interface import Base as Interface
+
+class portal_groups(Interface):
+ """Defines an interface for working with groups in an abstract manner.
+ Parallels the portal_membership interface of CMFCore"""
+## id = Attribute('id','Must be set to "portal_groups"')
+
+ def isGroup(u):
+ """Test if a user/group object is a group or not.
+ You must pass an object you get earlier with wrapUser() or wrapGroup()
+ """
+
+ def getGroupById(id):
+ """Returns the portal_groupdata-ish object for a group corresponding
+ to this id."""
+
+ def getGroupsByUserId(userid):
+ """Returns a list of the groups the user corresponding to 'userid' belongs to."""
+
+ def listGroups():
+ """Returns a list of the available portal_groupdata-ish objects."""
+
+ def listGroupIds():
+ """Returns a list of the available groups' ids (WITHOUT prefixes)."""
+
+ def listGroupNames():
+ """Returns a list of the available groups' names (ie. without prefixes)."""
+
+## def getPureUserNames():
+## """Get the usernames (ids) of only users. """
+
+## def getPureUsers():
+## """Get the actual (unwrapped) user objects of only users. """
+
+ def searchForGroups(REQUEST, **kw): # maybe searchGroups()?
+ """Return a list of groups meeting certain conditions. """
+ # arguments need to be better refined?
+
+ def addGroup(id, roles = [], groups = [], **kw):
+ """Create a group with the supplied id, roles, and groups.
+
+ Underlying user folder must support adding users via the usual Zope API.
+ Passwords for groups seem to be currently irrelevant in GRUF."""
+
+ def editGroup(id, roles = [], groups = [], **kw):
+ """Edit the given group with the supplied roles.
+
+ Underlying user folder must support editing users via the usual Zope API.
+ Passwords for groups seem to be currently irrelevant in GRUF.
+ One can supply additional named parameters to set group properties."""
+
+ def removeGroups(ids, keep_workspaces=0):
+ """Remove the group in the provided list (if possible).
+
+ Will by default remove this group's GroupWorkspace if it exists. You may
+ turn this off by specifying keep_workspaces=true.
+ Underlying user folder must support removing users via the usual Zope API."""
+
+ def setGroupOwnership(group, object):
+ """Make the object 'object' owned by group 'group' (a portal_groupdata-ish object)"""
+
+ def setGroupWorkspacesFolder(id=""):
+ """ Set the location of the Group Workspaces folder by id.
+
+ The Group Workspaces Folder contains all the group workspaces, just like the
+ Members folder contains all the member folders.
+
+ If anyone really cares, we can probably make the id work as a path as well,
+ but for the moment it's only an id for a folder in the portal root, just like the
+ corresponding MembershipTool functionality. """
+
+ def getGroupWorkspacesFolderId():
+ """ Get the Group Workspaces folder object's id.
+
+ The Group Workspaces Folder contains all the group workspaces, just like the
+ Members folder contains all the member folders. """
+
+ def getGroupWorkspacesFolder():
+ """ Get the Group Workspaces folder object.
+
+ The Group Workspaces Folder contains all the group workspaces, just like the
+ Members folder contains all the member folders. """
+
+ def toggleGroupWorkspacesCreation():
+ """ Toggles the flag for creation of a GroupWorkspaces folder upon first
+ use of the group. """
+
+ def getGroupWorkspacesCreationFlag():
+ """Return the (boolean) flag indicating whether the Groups Tool will create a group workspace
+ upon the next use of the group (if one doesn't exist). """
+
+ def getGroupWorkspaceType():
+ """Return the Type (as in TypesTool) to make the GroupWorkspace."""
+
+ def setGroupWorkspaceType(type):
+ """Set the Type (as in TypesTool) to make the GroupWorkspace. Expects the name of a Type."""
+
+ def createGrouparea(id):
+ """Create a space in the portal for the given group, much like member home
+ folders."""
+
+ def getGroupareaFolder(id):
+ """Returns the object of the group's work area."""
+
+ def getGroupareaURL(id):
+ """Returns the full URL to the group's work area."""
+
+ # and various roles things...
--- /dev/null
+GroupUserFolder
--- /dev/null
+<html metal:use-macro="here/main_template/macros/master">
+<body>
+<div metal:fill-slot="main" >
+
+ <div class="contentHeader">
+ <h1 tal:content="here/Title"> Title </h1>
+ <div class="contentBody">
+ <p>Here's the (unmutable) content of your MinimalFolderishType.</p>
+ <p>Have a nice plonish day ! :-)</p>
+ </div>
+ </div>
+
+</div>
+
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+## Script (Python) "change_password"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=password, confirm, domains=None
+##title=Change password
+##
+
+pass
+
+## This code is there because there's a bug in CMF that prevents
+## passwords to be changed if the User Folder doesn't store it in a __
+## attribute.
+## This includes User Folders such as LDAPUF, SimpleUF, and, of course, GRUF.
+## This also includes standard UF with password encryption !
+
+mt = context.portal_membership
+failMessage=context.portal_registration.testPasswordValidity(password, confirm)
+
+if failMessage:
+ return context.password_form(context,
+ context.REQUEST,
+ error=failMessage)
+context.REQUEST['AUTHENTICATED_USER'].changePassword(password,REQUEST=context.REQUEST)
+mt.credentialsChanged(password)
+return context.personalize_form(context,
+ context.REQUEST,
+ portal_status_message='Password changed.')
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
+ lang="en-US"
+ metal:use-macro="here/main_template/macros/master"
+ i18n:domain="plone">
+
+<body>
+
+ <div metal:fill-slot="main"
+ tal:define="Iterator python:modules['Products.CMFPlone'].IndexIterator;
+ Batch python:modules['Products.CMFPlone'].Batch;
+ group_submit request/group_submit|nothing;
+ b_size python:12;b_start python:0;b_start request/b_start | b_start;
+ search_submitted request/role_submit|nothing;
+ search_results python:test(search_submitted, here.portal_membership.searchMembers(
+ search_param=request.get('search_param',''),
+ search_term=request.get('search_term', '') ), None);">
+
+ <h1 i18n:translate="heading_currently_assigned_localroles">
+ Currently assigned local roles in folder
+ <span tal:content="here/title_or_id" i18n:name="folder">title</span>
+ </h1>
+
+ <p i18n:translate="description_current_localroles">
+ These users currently have local roles assigned in this folder:
+ </p>
+
+ <form class="group"
+ method="post"
+ name="deleterole"
+ action="folder_localrole_edit"
+ tal:attributes="action string:${here/absolute_url}/folder_localrole_edit">
+
+ <span class="legend" i18n:translate="legend_assigned_roles">
+ Assigned Roles
+ <span tal:content="here/title_or_id" i18n:name="folder">title</span>
+ </span>
+
+ <input type="hidden" name="change_type" value="delete" />
+ <input type="hidden" name="member_role" value="" />
+
+ <table class="listing" summary="Currently assigned local roles"
+ tal:define="username python:here.portal_membership.getAuthenticatedMember().getUserName();">
+ <thead>
+ <tr>
+ <th> </th>
+ <th i18n:translate="label_user_group_name">User/Group name</th>
+ <th i18n:translate="label_type">Type</th>
+ <th i18n:translate="label_roles">Role(s)</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="lrole python:here.acl_users.getLocalRolesForDisplay(here)">
+ <td>
+ <input class="noborder"
+ type="checkbox"
+ name="member_ids:list"
+ id="#"
+ value=""
+ tal:condition="python:lrole[0]!=username"
+ tal:attributes="value python:lrole[3];"
+ />
+ </td>
+
+ <td tal:content="python:lrole[0]">
+ groupname
+ </td>
+
+ <td tal:condition="python:lrole[2]=='group'"
+ i18n:translate="label_group">
+ Group
+ </td>
+ <td tal:condition="python:lrole[2]=='user'"
+ i18n:translate="label_user">
+ User
+ </td>
+
+ <td>
+ <tal:block tal:repeat="role python:lrole[1]">
+ <span i18n:translate=""
+ tal:content="role"
+ tal:omit-tag="">Role</span>
+ <span tal:condition="not: repeat/role/end"
+ tal:omit-tag="">, </span>
+ </tal:block>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <input class="context"
+ type="submit"
+ value="Delete Selected Role(s)"
+ i18n:attributes="value"
+ />
+ </form>
+
+ <metal:block tal:condition="python:test(search_submitted and not search_results, 1, 0)">
+ <h1 i18n:translate="heading_search_results">Search results</h1>
+ <p i18n:translate="no_members_found">
+ No members were found using your <strong>Search Criteria</strong>
+ </p>
+ <hr />
+ </metal:block>
+
+ <metal:block tal:condition="python:test(search_submitted and search_results, 1, 0)">
+
+ <h1 i18n:translate="heading_search_results">Search results</h1>
+
+ <p i18n:translate="description_localrole_select_member">
+ Select one or more Members, and a role to assign.
+ </p>
+
+ <metal:block tal:define="batch python:Batch(search_results, b_size, int(b_start), orphan=3)">
+
+ <form class="group"
+ method="post"
+ name="change_type"
+ action="folder_localrole_edit"
+ tal:attributes="action string:${here/absolute_url}/folder_localrole_edit">
+
+ <span class="legend" i18n:translate="legend_available_members">
+ Available Members
+ </span>
+
+ <input type="hidden" name="change_type" value="add" />
+
+ <!-- batch navigation -->
+ <div metal:use-macro="here/batch_macros/macros/navigation" />
+
+ <table class="listing" summary="Search results">
+ <thead>
+ <tr>
+ <th> </th>
+ <th i18n:translate="label_user_name">User Name</th>
+ <th i18n:translate="label_email_address">Email Address</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="member batch">
+ <td>
+ <input class="noborder"
+ type="checkbox"
+ name="member_ids:list"
+ id="#"
+ value=""
+ tal:attributes="value member/username;"
+ />
+ </td>
+
+ <td tal:content="member/username">username</td>
+ <td tal:content="member/email">email</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- batch navigation -->
+ <div metal:use-macro="here/batch_macros/macros/navigation" />
+
+ <div class="row">
+
+ <div class="label" i18n:translate="label_localrole_to_assign">
+ Role to assign
+ </div>
+
+ <div class="field">
+ <select name="member_role">
+ <option tal:repeat="lroles python:container.portal_membership.getCandidateLocalRoles(here)"
+ tal:attributes="value lroles"
+ tal:content="lroles"
+ i18n:translate="">
+ Role name
+ </option>
+ </select>
+ </div>
+
+ </div>
+
+ <div class="row">
+ <div class="label"> </div>
+ <div class="field">
+ <input class="context"
+ type="submit"
+ value="Assign Local Role to Selected User(s)"
+ i18n:attributes="value"
+ />
+ </div>
+ </div>
+
+ </form>
+
+ </metal:block>
+ </metal:block>
+
+ <div>
+ <tal:block tal:condition="python: (not search_submitted or
+ (search_submitted and not search_results))">
+
+ <h1 i18n:translate="heading_assign_localrole">
+ Assign local roles to folder
+ <tal:block tal:content="here/title_or_id" i18n:name="folder">title</tal:block>
+ </h1>
+
+ <p i18n:translate="description_assign_localrole">
+ A local role is a way of allowing other users into some or
+ all of your folders. These users can edit items, publish
+ them - et cetera, depending on what permissions you give
+ them.
+ <br />
+
+ Local roles are ideal in cooperation projects, and as every
+ item has a history and an undo option, it's easy to keep
+ track of the changes.
+
+ <br />
+
+ To give a person a local role in this folder, just search
+ for the person's name or email address in the form below,
+ and you will be presented with a page that will show you the
+ options available.
+ </p>
+
+ <form class="group"
+ method="post"
+ name="localrole"
+ action="folder_localrole_form"
+ tal:attributes="action string:${here/absolute_url}/${template/getId}" >
+
+ <span class="legend" i18n:translate="legend_search_terms">
+ Search Terms
+ </span>
+
+ <input type="hidden" name="role_submit" value="role_submit" />
+
+ <div class="row">
+ <div class="label" i18n:translate="label_search_by">
+ Search by
+ </div>
+
+ <div class="field">
+ <select name="search_param">
+ <option value="username" i18n:translate="label_user_name">
+ User Name
+ </option>
+ <option value="email" i18n:translate="label_email_address">
+ Email Address
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="label"
+ i18n:translate="label_search_term">
+ Search Term
+ </div>
+
+ <div class="field">
+ <input type="text"
+ name="search_term"
+ size="30"
+ />
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="label"> </div>
+ <div class="field">
+ <input class="context"
+ type="submit"
+ value="Perform Search"
+ i18n:attributes="value"
+ />
+ </div>
+ </div>
+
+ </form>
+ </tal:block>
+
+ <h1 i18n:translate="heading_available_groups">Available groups</h1>
+
+ <p i18n:translate="description_available_groups">
+ Groups are a convenient way to assign roles to a common set of
+ users. Select one or more Groups, and a role to assign.
+ </p>
+
+ <form class="group"
+ method="post"
+ name="change_type"
+ action="folder_localrole_edit"
+ tal:attributes="action string:${here/absolute_url}/folder_localrole_edit">
+
+ <span class="legend" i18n:translate="legend_available_groups">
+ Available Groups
+ </span>
+
+ <input type="hidden" name="change_type" value="add" />
+
+ <table class="listing" summary="Available groups">
+ <thead>
+ <tr>
+ <th> </th>
+ <th i18n:translate="">Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="member here/acl_users/getGroups">
+ <td>
+ <input class="noborder"
+ type="checkbox"
+ name="member_ids:list"
+ id="#"
+ value=""
+ tal:attributes="value member/getUserName;" />
+ </td>
+ <td tal:content="python:member.getUserNameWithoutGroupPrefix()">
+ groupname
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="row">
+ <div class="label" i18n:translate="label_localrole_to_assign">
+ Role to assign
+ </div>
+
+ <div class="field">
+ <select name="member_role">
+ <option tal:repeat="lroles python:container.portal_membership.getCandidateLocalRoles(here)"
+ tal:attributes="value lroles"
+ tal:content="lroles"
+ i18n:translate="">
+ Role name
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="label"> </div>
+ <div class="field">
+ <input class="context"
+ type="submit"
+ value="Assign Local Role to Selected Group(s)"
+ i18n:attributes="value"
+ />
+ </div>
+ </div>
+
+ </form>
+
+ </div>
+
+ </div> <!-- fill-slot -->
+
+</body>
+</html>
--- /dev/null
+## Script (Python) "getUsersInGroup"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=groupid
+##title=
+##
+
+users=context.acl_users.getUsers()
+prefix=context.acl_users.getGroupPrefix()
+
+avail=[]
+for user in users:
+ for group in user.getGroups():
+ if groupid==group or \
+ prefix+groupid==group:
+ avail.append(user)
+
+return avail
--- /dev/null
+## Script (Python) "gruf_ldap_required_fields"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=login
+##title=Mandatory / default LDAP attribute values
+##
+
+return {
+ "sn": login,
+ "cn": login,
+ }
--- /dev/null
+## Script (Python) "prefs_group_manage"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Manage groups
+##
+REQUEST=context.REQUEST
+groupstool=context.portal_groups
+
+groups=[group[len('group_'):]
+ for group in REQUEST.keys()
+ if group.startswith('group_')]
+
+for group in groups:
+ roles=REQUEST['group_'+group]
+ groupstool.editGroup(group, roles = roles, REQUEST=context.REQUEST, )
+
+
+delete=REQUEST.get('delete',[])
+groupstool.removeGroups(delete, REQUEST=context.REQUEST,)
+
+portal_status_message="Changes made."
+return state.set(portal_status_message=portal_status_message)
--- /dev/null
+[validators]
+validators =
+
+[actions]
+action.success = traverse_to:string:prefs_groups_overview
+action.failure = traverse_to:string:prefs_groups_overview
\ No newline at end of file
--- /dev/null
+Here is the placeholder for files providing Plone 2.0 compatibility for GRUF 3.
+This is the case, for example, for local roles form or control panel stuff.
+
+This skin is empty by now. You don't have to worry about it.
--- /dev/null
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
+ lang="en"
+ metal:use-macro="here/main_template/macros/master"
+ i18n:domain="plone">
+
+<metal:block fill-slot="top_slot"
+ tal:define="dummy python:request.set('enable_border',1)" />
+
+<body>
+
+ <div metal:fill-slot="main"
+ tal:define="Batch python:modules['Products.CMFPlone'].Batch;
+ username member/getUserName;
+ group_submit request/group_submit|nothing;
+ b_size python:12;b_start python:0;b_start request/b_start | b_start;
+ search_submitted request/role_submit|nothing;
+ search_results python:search_submitted and mtool.searchForMembers(
+ {request.get('search_param',''):
+ request.get('search_term', '')}) or None;">
+
+ <h1 i18n:translate="heading_currently_assigned_shares">
+ Current sharing permissions for
+ <span tal:content="here/title_or_id" i18n:name="folder">title</span>
+ </h1>
+
+ <p i18n:translate="description_share_folders_items_current_shares">
+ You can share the rights for both folders (including content) and single items.
+ These users have privileges here:
+ </p>
+
+ <fieldset tal:define="iroles python:here.plone_utils.getInheritedLocalRoles(here);"
+ tal:condition="iroles">
+
+ <legend i18n:translate="legend_acquired_roles">
+ Acquired roles
+ </legend>
+
+ <table class="listing" summary="Acquired roles">
+ <thead>
+ <tr>
+ <th i18n:translate="label_user_group_name">User/Group name</th>
+ <th i18n:translate="label_type">Type</th>
+ <th i18n:translate="label_roles">Role(s)</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="irole iroles">
+ <td tal:content="python:irole[0]">
+ groupname
+ </td>
+
+ <td tal:condition="python:irole[2]=='group'"
+ i18n:translate="label_group">
+ Group
+ </td>
+ <td tal:condition="python:irole[2]=='user'"
+ i18n:translate="label_user">
+ User
+ </td>
+
+ <td>
+ <tal:block tal:repeat="role python:irole[1]">
+ <span i18n:translate=""
+ tal:content="role"
+ tal:omit-tag="">Role</span>
+ <span tal:condition="not: repeat/role/end"
+ tal:omit-tag="">, </span>
+ </tal:block>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ </fieldset>
+
+ <form method="post"
+ name="deleterole"
+ action="folder_localrole_edit"
+ tal:attributes="action string:$here_url/folder_localrole_edit">
+
+ <fieldset>
+
+ <legend i18n:translate="legend_assigned_roles">
+ Assigned Roles
+ <span tal:content="here/title_or_id" i18n:name="folder">title</span>
+ </legend>
+
+ <input type="hidden" name="change_type" value="delete" />
+ <input type="hidden" name="member_role" value="" />
+
+ <table class="listing" summary="Currently assigned local roles">
+ <thead>
+ <tr>
+ <th>
+ <input type="checkbox"
+ onclick="javascript:toggleSelect(this, 'member_ids:list', false, 'deleterole');"
+ name="alr_toggle"
+ value="#"
+ id="alr_toggle"
+ class="noborder"
+ />
+ </th>
+ <th i18n:translate="label_user_group_name">User/Group name</th>
+ <th i18n:translate="label_type">Type</th>
+ <th i18n:translate="label_roles">Role(s)</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="lrole python:here.acl_users.getLocalRolesForDisplay(here)">
+ <td class="field">
+ <label class="hiddenLabel" for="member_ids:list"
+ i18n:translate="label_select_usergroup">
+ select user/group <span tal:content="python:lrole[3]" i18n:name="role"/>
+ </label>
+ <input class="formSelection"
+ type="checkbox"
+ name="member_ids:list"
+ id="#"
+ value=""
+ tal:condition="python:lrole[0]!=username"
+ tal:attributes="value python:lrole[3];"
+ />
+ </td>
+
+ <td tal:content="python:lrole[0]">
+ groupname
+ </td>
+
+ <td tal:condition="python:lrole[2]=='group'"
+ i18n:translate="label_group">
+ Group
+ </td>
+ <td tal:condition="python:lrole[2]=='user'"
+ i18n:translate="label_user">
+ User
+ </td>
+
+ <td>
+ <tal:block tal:repeat="role python:lrole[1]">
+ <span i18n:translate=""
+ tal:content="role"
+ tal:omit-tag="">Role</span>
+ <span tal:condition="not: repeat/role/end"
+ tal:omit-tag="">, </span>
+ </tal:block>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="submit">
+ <input class="context"
+ type="submit"
+ value="Delete Selected Role(s)"
+ i18n:attributes="value"
+ />
+ </div>
+
+ </fieldset>
+
+ </form>
+
+ <metal:block tal:condition="python:test(search_submitted and not search_results, 1, 0)">
+ <h1 i18n:translate="heading_search_results">Search results</h1>
+ <p i18n:translate="no_members_found">
+ No members were found using your <strong>Search Criteria</strong>
+ </p>
+ <hr />
+ </metal:block>
+
+ <metal:block tal:condition="python:test(search_submitted and search_results, 1, 0)">
+
+ <h1 i18n:translate="heading_search_results">Search results</h1>
+
+ <p i18n:translate="description_localrole_select_member">
+ Select one or more people, and a role to assign.
+ </p>
+
+ <metal:block tal:define="batch python:Batch(search_results, b_size, int(b_start), orphan=3);
+ nResults python:len(search_results);">
+
+ <form method="post"
+ name="change_type"
+ action="folder_localrole_edit"
+ tal:attributes="action string:$here_url/folder_localrole_edit">
+
+ <fieldset>
+
+ <legend i18n:translate="legend_available_members">Available Members</legend>
+
+ <input type="hidden" name="change_type" value="add" />
+
+ <!-- batch navigation -->
+ <div metal:use-macro="here/batch_macros/macros/navigation" />
+
+ <table class="listing" summary="Search results">
+ <thead>
+ <tr>
+ <th>
+ <input type="checkbox"
+ onclick="javascript:toggleSelect(this, 'member_ids:list', false, 'change_type');"
+ name="alr_toggle"
+ value="#"
+ id="alr_toggle"
+ class="noborder"
+ />
+ </th>
+ <th i18n:translate="label_user_name">User Name</th>
+ <th i18n:translate="label_email_address">Email Address</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="member batch">
+ <td class="field" tal:define="global member_username member/getUserName">
+ <label class="hiddenLabel" for="member_ids:list"
+ i18n:translate="label_select_user">
+ select user <span tal:content="member_username" i18n:name="user" />
+ </label>
+ <input class="formSelection"
+ type="checkbox"
+ name="member_ids:list"
+ id="#"
+ value=""
+ tal:attributes="value member_username;
+ checked python:nResults==1;"
+ />
+ </td>
+
+ <td tal:content="python:member_username">username</td>
+ <td tal:content="member/email">email</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- batch navigation -->
+ <div metal:use-macro="here/batch_macros/macros/navigation" />
+
+ <div class="field">
+
+ <label for="user_member_role" i18n:translate="label_localrole_to_assign">
+ Role to assign
+ </label>
+
+ <select name="member_role:list"
+ id="user_member_role"
+ multiple="multiple">
+ <option tal:repeat="lroles python:mtool.getCandidateLocalRoles(here)"
+ tal:attributes="value lroles"
+ tal:content="lroles"
+ i18n:translate="">
+ Role name
+ </option>
+ </select>
+
+ </div>
+
+ <div class="submit">
+ <input class="context"
+ type="submit"
+ value="Assign Local Role to Selected User(s)"
+ i18n:attributes="value"
+ />
+ </div>
+
+ </fieldset>
+
+ </form>
+
+ </metal:block>
+ </metal:block>
+
+ <div>
+ <tal:block tal:condition="python: (not search_submitted or
+ (search_submitted and not search_results))">
+
+ <h1 i18n:translate="heading_add_sharing_permissions">
+ Add sharing permissions for
+ <tal:block tal:content="here/title_or_id" i18n:name="item">title</tal:block>
+ </h1>
+
+
+ <p i18n:translate="description_sharing_item">
+ Sharing is an easy way to allow others access to collaborate with you
+ on your content.
+
+ To share this item, search for the person's
+ name or email address in the form below, and assign them an appropriate role.
+ The most common use is to give people Manager permissions, which means they
+ have full control of this item and its contents (if any).
+ </p>
+
+ <form method="post"
+ name="localrole"
+ action="folder_localrole_form"
+ tal:attributes="action string:$here_url/${template/getId}" >
+
+ <fieldset>
+
+ <legend i18n:translate="legend_search_terms">Search Terms</legend>
+
+ <input type="hidden" name="role_submit" value="role_submit" />
+
+ <div class="field">
+ <label for="search_param" i18n:translate="label_search_by">
+ Search by
+ </label>
+
+ <select name="search_param"
+ id="search_param">
+ <option value="name" i18n:translate="label_user_name">
+ User Name
+ </option>
+ <option value="email" i18n:translate="label_email_address">
+ Email Address
+ </option>
+ </select>
+
+ </div>
+
+ <div class="field">
+ <label for="search_term" i18n:translate="label_search_term">
+ Search Term
+ </label>
+
+ <input type="text"
+ id="search_term"
+ name="search_term"
+ size="30"
+ />
+ </div>
+
+ <div class="submit">
+ <input class="context"
+ type="submit"
+ value="Perform Search"
+ i18n:attributes="value"
+ />
+ </div>
+
+ </fieldset>
+
+ </form>
+ </tal:block>
+
+ <tal:groupshares define="grouplist gtool/listGroups"
+ condition="grouplist">
+
+ <h1 i18n:translate="heading_group_shares">Add sharing permissions to groups</h1>
+
+ <p i18n:translate="description_group_shares">
+ Groups are a convenient way to share items to a common set of
+ users. Select one or more groups, and a role to assign.
+ </p>
+
+ <form method="post"
+ name="change_type_group"
+ action="folder_localrole_edit"
+ tal:attributes="action string:$here_url/folder_localrole_edit">
+
+ <fieldset>
+
+ <legend i18n:translate="legend_available_groups">
+ Available Groups
+ </legend>
+
+ <input type="hidden" name="change_type" value="add" />
+
+ <table class="listing" summary="Available groups">
+ <thead>
+ <tr>
+ <th>
+ <input type="checkbox"
+ onclick="javascript:toggleSelect(this, 'member_ids:list', false, 'change_type_group');"
+ name="alr_toggle"
+ value="#"
+ id="alr_toggle"
+ class="noborder"
+ />
+ </th>
+ <th i18n:translate="listingheader_name">Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr tal:repeat="group grouplist">
+ <td tal:define="global group_name group/getUserId">
+ <label class="hiddenLabel" for="member_ids:list"
+ i18n:translate="label_select_group">
+ select group <span tal:content="group_name" i18n:name="name"/>
+ </label>
+ <input class="formSelection"
+ type="checkbox"
+ name="member_ids:list"
+ id="#"
+ value=""
+ tal:attributes="value group_name;" />
+ </td>
+ <td tal:content="group/getUserNameWithoutGroupPrefix">
+ groupname
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="field">
+
+ <label for="group_member_role" i18n:translate="label_localrole_to_assign">
+ Role to assign
+ </label>
+
+ <select name="member_role:list"
+ id="group_member_role"
+ multiple="multiple">
+ <option tal:repeat="lroles python:mtool.getCandidateLocalRoles(here)"
+ tal:attributes="value lroles"
+ tal:content="lroles"
+ i18n:translate="">
+ Role name
+ </option>
+ </select>
+ </div>
+
+ <div class="submit">
+ <input class="context"
+ type="submit"
+ value="Assign Local Role to Selected Group(s)"
+ i18n:attributes="value"
+ />
+ </div>
+
+ </fieldset>
+
+ </form>
+
+ </tal:groupshares>
+
+ <div metal:use-macro="here/document_byline/macros/byline">
+ Get the byline - contains details about author and modification date.
+ </div>
+
+ </div>
+
+ </div>
+
+</body>
+</html>
--- /dev/null
+Création branche pour compat Zope-2.12
+--This line, and those below, will be ignored--
+
+A http://svn.cri.ensmp.fr/svn/GroupUserFolder/branches/zope-2.12