Ajout de GroupUserFolder extrait de l'état suivant :
authorBenoît Pin <benoit.pin@gmail.com>
Mon, 25 Oct 2010 10:58:48 +0000 (12:58 +0200)
committerBenoît Pin <benoit.pin@gmail.com>
Mon, 25 Oct 2010 10:58:48 +0000 (12:58 +0200)
URL: http://svn.cri.ensmp.fr/svn/GroupUserFolder/branches/3.55.1
Repository Root: http://svn.cri.ensmp.fr/svn/GroupUserFolder
Repository UUID: 1bf790b2-e4d4-0310-9a6c-c8a412c25dae
Revision: 591
Node Kind: directory
Schedule: normal
Last Changed Author: pin
Last Changed Rev: 591
Last Changed Date: 2009-02-11 18:49:59 +0100 (Mer, 11 fév 2009)

95 files changed:
ABOUT [new file with mode: 0644]
CHANGES [new file with mode: 0644]
CONTRIBUTORS [new file with mode: 0644]
DynaList.py [new file with mode: 0644]
Extensions/Install.py [new file with mode: 0644]
Extensions/__init__.py [new file with mode: 0644]
GRUFFolder.py [new file with mode: 0644]
GRUFUser.py [new file with mode: 0644]
GroupDataTool.py [new file with mode: 0644]
GroupUserFolder.py [new file with mode: 0644]
GroupsTool.py [new file with mode: 0644]
GroupsToolPermissions.py [new file with mode: 0644]
INSTALL.txt [new file with mode: 0644]
Installation.py [new file with mode: 0644]
LDAPGroupFolder.py [new file with mode: 0755]
LDAPUserFolderAdapter.py [new file with mode: 0755]
LICENSE [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
Log.py [new file with mode: 0644]
PRODUCT_NAME [new file with mode: 0644]
PatchCatalogTool.py [new file with mode: 0644]
PloneFeaturePreview.py [new file with mode: 0755]
README.txt [new file with mode: 0644]
TESTED_WITH [new file with mode: 0644]
TODO [new file with mode: 0644]
__init__.py [new file with mode: 0644]
class_utility.py [new file with mode: 0644]
cvs2cl.pl [new file with mode: 0755]
debian/changelog [new file with mode: 0644]
debian/config [new file with mode: 0755]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/postinst [new file with mode: 0755]
debian/prerm [new file with mode: 0755]
debian/rules [new file with mode: 0755]
debian/templates [new file with mode: 0644]
debian/watch [new file with mode: 0644]
design.txt [new file with mode: 0644]
doc/FAQ [new file with mode: 0644]
doc/GRUF3.0.stx [new file with mode: 0644]
doc/GRUFLogo.png [new file with mode: 0644]
doc/SCREENSHOTS [new file with mode: 0644]
doc/folder_contents.png [new file with mode: 0644]
doc/icon.png [new file with mode: 0644]
doc/interview.txt [new file with mode: 0644]
doc/menu.png [new file with mode: 0644]
doc/tab_audit.png [new file with mode: 0644]
doc/tab_groups.png [new file with mode: 0644]
doc/tab_overview.png [new file with mode: 0644]
doc/tab_sources.png [new file with mode: 0644]
doc/tab_users.png [new file with mode: 0644]
doc/user_edit.png [new file with mode: 0644]
dtml/GRUFFolder_main.dtml [new file with mode: 0644]
dtml/GRUF_audit.zpt [new file with mode: 0644]
dtml/GRUF_contents.zpt [new file with mode: 0644]
dtml/GRUF_groups.zpt [new file with mode: 0644]
dtml/GRUF_newusers.zpt [new file with mode: 0644]
dtml/GRUF_overview.zpt [new file with mode: 0644]
dtml/GRUF_user.zpt [new file with mode: 0644]
dtml/GRUF_users.zpt [new file with mode: 0644]
dtml/GRUF_wizard.zpt [new file with mode: 0644]
dtml/addLDAPGroupFolder.dtml [new file with mode: 0755]
dtml/configureGroupsTool.dtml [new file with mode: 0644]
dtml/explainGroupDataTool.dtml [new file with mode: 0644]
dtml/explainGroupsTool.dtml [new file with mode: 0644]
dtml/groups.dtml [new file with mode: 0755]
dtml/roles.png [new file with mode: 0644]
global_symbols.py [new file with mode: 0644]
interfaces/.cvsignore [new file with mode: 0644]
interfaces/IUserFolder.py [new file with mode: 0644]
interfaces/__init__.py [new file with mode: 0644]
interfaces/portal_groupdata.py [new file with mode: 0644]
interfaces/portal_groups.py [new file with mode: 0644]
product.txt [new file with mode: 0644]
skins/gruf/GroupSpaceFolderishType_view.pt.old [new file with mode: 0644]
skins/gruf/change_password.py [new file with mode: 0644]
skins/gruf/defaultGroup.gif [new file with mode: 0644]
skins/gruf/folder_localrole_form_plone1.pt [new file with mode: 0644]
skins/gruf/getUsersInGroup.py [new file with mode: 0644]
skins/gruf/gruf_ldap_required_fields.py [new file with mode: 0755]
skins/gruf/prefs_group_manage.cpy [new file with mode: 0755]
skins/gruf/prefs_group_manage.cpy.metadata [new file with mode: 0755]
skins/gruf_plone_2_0/README.txt [new file with mode: 0755]
skins/gruf_plone_2_0/folder_localrole_form.pt [new file with mode: 0644]
svn-commit.tmp [new file with mode: 0644]
tool.gif [new file with mode: 0644]
version.txt [new file with mode: 0644]
www/GRUFGroups.gif [new file with mode: 0644]
www/GRUFUsers.gif [new file with mode: 0644]
www/GroupUserFolder.gif [new file with mode: 0644]
www/LDAPGroupFolder.gif [new file with mode: 0644]
www/down_arrow.gif [new file with mode: 0644]
www/down_arrow_grey.gif [new file with mode: 0644]
www/up_arrow.gif [new file with mode: 0644]
www/up_arrow_grey.gif [new file with mode: 0644]

diff --git a/ABOUT b/ABOUT
new file mode 100644 (file)
index 0000000..bee4873
--- /dev/null
+++ b/ABOUT
@@ -0,0 +1 @@
+A Zope Product that manages Groups of Users
diff --git a/CHANGES b/CHANGES
new file mode 100644 (file)
index 0000000..b8506de
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,375 @@
+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.
+
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
new file mode 100644 (file)
index 0000000..97b6f84
--- /dev/null
@@ -0,0 +1,20 @@
+
+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
diff --git a/DynaList.py b/DynaList.py
new file mode 100644 (file)
index 0000000..22b1be6
--- /dev/null
@@ -0,0 +1,96 @@
+# -*- 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)
diff --git a/Extensions/Install.py b/Extensions/Install.py
new file mode 100644 (file)
index 0000000..87de596
--- /dev/null
@@ -0,0 +1,126 @@
+# -*- 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()
diff --git a/Extensions/__init__.py b/Extensions/__init__.py
new file mode 100644 (file)
index 0000000..9889842
--- /dev/null
@@ -0,0 +1,24 @@
+# -*- 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'
diff --git a/GRUFFolder.py b/GRUFFolder.py
new file mode 100644 (file)
index 0000000..6e3fef6
--- /dev/null
@@ -0,0 +1,299 @@
+# -*- 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)
diff --git a/GRUFUser.py b/GRUFUser.py
new file mode 100644 (file)
index 0000000..4142c42
--- /dev/null
@@ -0,0 +1,935 @@
+# -*- 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)
diff --git a/GroupDataTool.py b/GroupDataTool.py
new file mode 100644 (file)
index 0000000..9b05326
--- /dev/null
@@ -0,0 +1,443 @@
+# -*- 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)
diff --git a/GroupUserFolder.py b/GroupUserFolder.py
new file mode 100644 (file)
index 0000000..8d6d85a
--- /dev/null
@@ -0,0 +1,2806 @@
+# -*- 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)
diff --git a/GroupsTool.py b/GroupsTool.py
new file mode 100644 (file)
index 0000000..e76caa1
--- /dev/null
@@ -0,0 +1,495 @@
+# -*- 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)
diff --git a/GroupsToolPermissions.py b/GroupsToolPermissions.py
new file mode 100644 (file)
index 0000000..bc6b1b6
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- 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'))
diff --git a/INSTALL.txt b/INSTALL.txt
new file mode 100644 (file)
index 0000000..7373ab1
--- /dev/null
@@ -0,0 +1,16 @@
+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.
diff --git a/Installation.py b/Installation.py
new file mode 100644 (file)
index 0000000..1be3578
--- /dev/null
@@ -0,0 +1,247 @@
+# -*- 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 !")
diff --git a/LDAPGroupFolder.py b/LDAPGroupFolder.py
new file mode 100755 (executable)
index 0000000..a269384
--- /dev/null
@@ -0,0 +1,393 @@
+# -*- 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)
diff --git a/LDAPUserFolderAdapter.py b/LDAPUserFolderAdapter.py
new file mode 100755 (executable)
index 0000000..642cdb8
--- /dev/null
@@ -0,0 +1,215 @@
+# -*- 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)
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..5fe2d42
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,57 @@
+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
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..5fe2d42
--- /dev/null
@@ -0,0 +1,57 @@
+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
diff --git a/Log.py b/Log.py
new file mode 100644 (file)
index 0000000..9da05b5
--- /dev/null
+++ b/Log.py
@@ -0,0 +1,195 @@
+# -*- 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   ",
+    }
diff --git a/PRODUCT_NAME b/PRODUCT_NAME
new file mode 100644 (file)
index 0000000..aaad7b8
--- /dev/null
@@ -0,0 +1 @@
+GroupUserFolder
diff --git a/PatchCatalogTool.py b/PatchCatalogTool.py
new file mode 100644 (file)
index 0000000..a9d54e9
--- /dev/null
@@ -0,0 +1,23 @@
+"""
+$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
diff --git a/PloneFeaturePreview.py b/PloneFeaturePreview.py
new file mode 100755 (executable)
index 0000000..0cf9854
--- /dev/null
@@ -0,0 +1,271 @@
+# -*- 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!")
+
+
+
diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..bf9168a
--- /dev/null
@@ -0,0 +1,118 @@
+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.
+
diff --git a/TESTED_WITH b/TESTED_WITH
new file mode 100644 (file)
index 0000000..5f4a809
--- /dev/null
@@ -0,0 +1,21 @@
+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
+
+
+
+
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..924a8e9
--- /dev/null
+++ b/TODO
@@ -0,0 +1,82 @@
+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
+
diff --git a/__init__.py b/__init__.py
new file mode 100644 (file)
index 0000000..e7603ac
--- /dev/null
@@ -0,0 +1,128 @@
+# -*- 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.")
diff --git a/class_utility.py b/class_utility.py
new file mode 100644 (file)
index 0000000..79357bd
--- /dev/null
@@ -0,0 +1,197 @@
+# -*- 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()
diff --git a/cvs2cl.pl b/cvs2cl.pl
new file mode 100755 (executable)
index 0000000..51371b9
--- /dev/null
+++ b/cvs2cl.pl
@@ -0,0 +1,1995 @@
+#!/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/&/&amp;/g;
+  $txt =~ s/</&lt;/g;
+  $txt =~ s/>/&gt;/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...
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..5d45ed1
--- /dev/null
@@ -0,0 +1,6 @@
+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
+
diff --git a/debian/config b/debian/config
new file mode 100755 (executable)
index 0000000..575a51b
--- /dev/null
@@ -0,0 +1,22 @@
+#!/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
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..4e94e29
--- /dev/null
@@ -0,0 +1,22 @@
+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.
+
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..5a2656f
--- /dev/null
@@ -0,0 +1,18 @@
+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
+
+    
diff --git a/debian/postinst b/debian/postinst
new file mode 100755 (executable)
index 0000000..e254c5a
--- /dev/null
@@ -0,0 +1,50 @@
+#! /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
diff --git a/debian/prerm b/debian/prerm
new file mode 100755 (executable)
index 0000000..8cd992c
--- /dev/null
@@ -0,0 +1,40 @@
+#! /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
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..866f9f1
--- /dev/null
@@ -0,0 +1,89 @@
+#!/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
+
diff --git a/debian/templates b/debian/templates
new file mode 100644 (file)
index 0000000..1ec4847
--- /dev/null
@@ -0,0 +1,7 @@
+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.
diff --git a/debian/watch b/debian/watch
new file mode 100644 (file)
index 0000000..3ed49e9
--- /dev/null
@@ -0,0 +1,5 @@
+# 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
diff --git a/design.txt b/design.txt
new file mode 100644 (file)
index 0000000..871edc0
--- /dev/null
@@ -0,0 +1,34 @@
+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 ;)
+
+
+
diff --git a/doc/FAQ b/doc/FAQ
new file mode 100644 (file)
index 0000000..301cefb
--- /dev/null
+++ b/doc/FAQ
@@ -0,0 +1,43 @@
+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
diff --git a/doc/GRUF3.0.stx b/doc/GRUF3.0.stx
new file mode 100644 (file)
index 0000000..50e77fb
--- /dev/null
@@ -0,0 +1,80 @@
+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 !
+
+
diff --git a/doc/GRUFLogo.png b/doc/GRUFLogo.png
new file mode 100644 (file)
index 0000000..c6aa14d
Binary files /dev/null and b/doc/GRUFLogo.png differ
diff --git a/doc/SCREENSHOTS b/doc/SCREENSHOTS
new file mode 100644 (file)
index 0000000..778a4b8
--- /dev/null
@@ -0,0 +1,32 @@
+.. 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
+
diff --git a/doc/folder_contents.png b/doc/folder_contents.png
new file mode 100644 (file)
index 0000000..390ccb7
Binary files /dev/null and b/doc/folder_contents.png differ
diff --git a/doc/icon.png b/doc/icon.png
new file mode 100644 (file)
index 0000000..c6aa14d
Binary files /dev/null and b/doc/icon.png differ
diff --git a/doc/interview.txt b/doc/interview.txt
new file mode 100644 (file)
index 0000000..965da26
--- /dev/null
@@ -0,0 +1,111 @@
+(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 !
+
diff --git a/doc/menu.png b/doc/menu.png
new file mode 100644 (file)
index 0000000..cb25484
Binary files /dev/null and b/doc/menu.png differ
diff --git a/doc/tab_audit.png b/doc/tab_audit.png
new file mode 100644 (file)
index 0000000..58d78f1
Binary files /dev/null and b/doc/tab_audit.png differ
diff --git a/doc/tab_groups.png b/doc/tab_groups.png
new file mode 100644 (file)
index 0000000..c515feb
Binary files /dev/null and b/doc/tab_groups.png differ
diff --git a/doc/tab_overview.png b/doc/tab_overview.png
new file mode 100644 (file)
index 0000000..5779360
Binary files /dev/null and b/doc/tab_overview.png differ
diff --git a/doc/tab_sources.png b/doc/tab_sources.png
new file mode 100644 (file)
index 0000000..4666f44
Binary files /dev/null and b/doc/tab_sources.png differ
diff --git a/doc/tab_users.png b/doc/tab_users.png
new file mode 100644 (file)
index 0000000..2bc6e9c
Binary files /dev/null and b/doc/tab_users.png differ
diff --git a/doc/user_edit.png b/doc/user_edit.png
new file mode 100644 (file)
index 0000000..9b8bea5
Binary files /dev/null and b/doc/user_edit.png differ
diff --git a/dtml/GRUFFolder_main.dtml b/dtml/GRUFFolder_main.dtml
new file mode 100644 (file)
index 0000000..feb89fb
--- /dev/null
@@ -0,0 +1,275 @@
+<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">&nbsp;</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>
+  &nbsp;
+  </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>
+  &nbsp;
+  </dtml-if>
+  <dtml-except>
+  &nbsp;
+  </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>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dtml/GRUF_audit.zpt b/dtml/GRUF_audit.zpt
new file mode 100644 (file)
index 0000000..26dfdc0
--- /dev/null
@@ -0,0 +1,236 @@
+  <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)">&nbsp;</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>
diff --git a/dtml/GRUF_contents.zpt b/dtml/GRUF_contents.zpt
new file mode 100644 (file)
index 0000000..7641a21
--- /dev/null
@@ -0,0 +1,216 @@
+<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">&nbsp;
+              <a href="Groups/acl_users/manage_workspace" tal:content="here/Groups/acl_users/meta_type">Type</a>
+          </td>
+          <td></td>
+          <td bgcolor="#EEEEEE">&nbsp;</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;">&nbsp;
+              <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>
+            &nbsp;
+            <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">&nbsp;</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>
diff --git a/dtml/GRUF_groups.zpt b/dtml/GRUF_groups.zpt
new file mode 100644 (file)
index 0000000..64fe589
--- /dev/null
@@ -0,0 +1,267 @@
+<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">&nbsp;
+            <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>&nbsp;</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">&nbsp;</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" />
+                    &nbsp;
+                    <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>
diff --git a/dtml/GRUF_newusers.zpt b/dtml/GRUF_newusers.zpt
new file mode 100644 (file)
index 0000000..93a1092
--- /dev/null
@@ -0,0 +1,32 @@
+  <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>
diff --git a/dtml/GRUF_overview.zpt b/dtml/GRUF_overview.zpt
new file mode 100644 (file)
index 0000000..9db51db
--- /dev/null
@@ -0,0 +1,208 @@
+<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">&nbsp;<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>
+      &nbsp;<font color="" tal:attributes="color here/user_color"><img src="img_user">&nbsp;Users appear this way</font><br />
+      &nbsp;<font color="" tal:attributes="color here/group_color"><img src="img_group">&nbsp;Groups appear this way</font><br />
+    &nbsp;<font color="" tal:attributes="color here/group_color"><i><img src="img_group">&nbsp;Nested groups (ie. groups inside groups) appear this way</i></font><br />
+      &nbsp;<font color="" tal:attributes="color here/role_color">User roles appear this way</font><br />
+      &nbsp;<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>
+
diff --git a/dtml/GRUF_user.zpt b/dtml/GRUF_user.zpt
new file mode 100644 (file)
index 0000000..56937b7
--- /dev/null
@@ -0,0 +1,247 @@
+  <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">&nbsp;
+        <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
+                              &nbsp;
+                              <input type="submit" name="deleteUser:method" value="Delete" />
+                                <br>&nbsp;
+                            </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>
+
+  
diff --git a/dtml/GRUF_users.zpt b/dtml/GRUF_users.zpt
new file mode 100644 (file)
index 0000000..b80223d
--- /dev/null
@@ -0,0 +1,340 @@
+    <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">
+            &nbsp;
+            <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>&nbsp;</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">&nbsp;</div></td>
+            <td>
+              <div class="list-item">Batch user creation list:</div>
+            </td>
+          </tr>
+          <tr>
+            <td><div class="list-item">&nbsp;</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">&nbsp;</div></td>
+            <td>
+              <div class="list-item">Default password:</div>
+            </td>
+          </tr>
+          <tr>
+            <td><div class="list-item">&nbsp;</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>
diff --git a/dtml/GRUF_wizard.zpt b/dtml/GRUF_wizard.zpt
new file mode 100644 (file)
index 0000000..6303879
--- /dev/null
@@ -0,0 +1,127 @@
+    <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>
diff --git a/dtml/addLDAPGroupFolder.dtml b/dtml/addLDAPGroupFolder.dtml
new file mode 100755 (executable)
index 0000000..7a92739
--- /dev/null
@@ -0,0 +1,55 @@
+<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>&nbsp;</td>
+        <td>
+          <br />
+          <input type="SUBMIT" value=" Add ">
+        </td>
+      </tr>
+    
+    </table>
+</form>
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/configureGroupsTool.dtml b/dtml/configureGroupsTool.dtml
new file mode 100644 (file)
index 0000000..fd0fc6c
--- /dev/null
@@ -0,0 +1,52 @@
+<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>
diff --git a/dtml/explainGroupDataTool.dtml b/dtml/explainGroupDataTool.dtml
new file mode 100644 (file)
index 0000000..c89c385
--- /dev/null
@@ -0,0 +1,10 @@
+<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>
diff --git a/dtml/explainGroupsTool.dtml b/dtml/explainGroupsTool.dtml
new file mode 100644 (file)
index 0000000..fe1ae61
--- /dev/null
@@ -0,0 +1,11 @@
+<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>
diff --git a/dtml/groups.dtml b/dtml/groups.dtml
new file mode 100755 (executable)
index 0000000..432b875
--- /dev/null
@@ -0,0 +1,224 @@
+<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">&nbsp;</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">&nbsp;</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">&nbsp;</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" />&nbsp;
+      </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">&nbsp;</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">&nbsp;</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>&nbsp;</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... &nbsp; 
+      </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 &nbsp;
+      </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>
diff --git a/dtml/roles.png b/dtml/roles.png
new file mode 100644 (file)
index 0000000..fd2456b
Binary files /dev/null and b/dtml/roles.png differ
diff --git a/global_symbols.py b/global_symbols.py
new file mode 100644 (file)
index 0000000..83d5321
--- /dev/null
@@ -0,0 +1,91 @@
+# -*- 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
diff --git a/interfaces/.cvsignore b/interfaces/.cvsignore
new file mode 100644 (file)
index 0000000..f3d74a9
--- /dev/null
@@ -0,0 +1,2 @@
+*.pyc
+*~
diff --git a/interfaces/IUserFolder.py b/interfaces/IUserFolder.py
new file mode 100644 (file)
index 0000000..9892274
--- /dev/null
@@ -0,0 +1,614 @@
+# -*- 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"""
diff --git a/interfaces/__init__.py b/interfaces/__init__.py
new file mode 100644 (file)
index 0000000..cb3bafe
--- /dev/null
@@ -0,0 +1,26 @@
+# -*- 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
diff --git a/interfaces/portal_groupdata.py b/interfaces/portal_groupdata.py
new file mode 100644 (file)
index 0000000..c0786a2
--- /dev/null
@@ -0,0 +1,93 @@
+# -*- 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)."""
diff --git a/interfaces/portal_groups.py b/interfaces/portal_groups.py
new file mode 100644 (file)
index 0000000..2be03ae
--- /dev/null
@@ -0,0 +1,144 @@
+# -*- 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...
diff --git a/product.txt b/product.txt
new file mode 100644 (file)
index 0000000..aaad7b8
--- /dev/null
@@ -0,0 +1 @@
+GroupUserFolder
diff --git a/skins/gruf/GroupSpaceFolderishType_view.pt.old b/skins/gruf/GroupSpaceFolderishType_view.pt.old
new file mode 100644 (file)
index 0000000..79c1267
--- /dev/null
@@ -0,0 +1,16 @@
+<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
diff --git a/skins/gruf/change_password.py b/skins/gruf/change_password.py
new file mode 100644 (file)
index 0000000..cd3f97f
--- /dev/null
@@ -0,0 +1,31 @@
+## 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.')
+
diff --git a/skins/gruf/defaultGroup.gif b/skins/gruf/defaultGroup.gif
new file mode 100644 (file)
index 0000000..eccbeb6
Binary files /dev/null and b/skins/gruf/defaultGroup.gif differ
diff --git a/skins/gruf/folder_localrole_form_plone1.pt b/skins/gruf/folder_localrole_form_plone1.pt
new file mode 100644 (file)
index 0000000..c4d8e19
--- /dev/null
@@ -0,0 +1,358 @@
+<!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>&nbsp;</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>&nbsp;</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">&nbsp;</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">&nbsp;</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>&nbsp;</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">&nbsp;</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>
diff --git a/skins/gruf/getUsersInGroup.py b/skins/gruf/getUsersInGroup.py
new file mode 100644 (file)
index 0000000..358a744
--- /dev/null
@@ -0,0 +1,21 @@
+## 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
diff --git a/skins/gruf/gruf_ldap_required_fields.py b/skins/gruf/gruf_ldap_required_fields.py
new file mode 100755 (executable)
index 0000000..0bb76f8
--- /dev/null
@@ -0,0 +1,14 @@
+## 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,
+  }
diff --git a/skins/gruf/prefs_group_manage.cpy b/skins/gruf/prefs_group_manage.cpy
new file mode 100755 (executable)
index 0000000..cb9e104
--- /dev/null
@@ -0,0 +1,26 @@
+## 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)
diff --git a/skins/gruf/prefs_group_manage.cpy.metadata b/skins/gruf/prefs_group_manage.cpy.metadata
new file mode 100755 (executable)
index 0000000..8bda807
--- /dev/null
@@ -0,0 +1,6 @@
+[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
diff --git a/skins/gruf_plone_2_0/README.txt b/skins/gruf_plone_2_0/README.txt
new file mode 100755 (executable)
index 0000000..2b7785f
--- /dev/null
@@ -0,0 +1,4 @@
+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.
diff --git a/skins/gruf_plone_2_0/folder_localrole_form.pt b/skins/gruf_plone_2_0/folder_localrole_form.pt
new file mode 100644 (file)
index 0000000..ed0d362
--- /dev/null
@@ -0,0 +1,445 @@
+<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>
diff --git a/svn-commit.tmp b/svn-commit.tmp
new file mode 100644 (file)
index 0000000..5a8a033
--- /dev/null
@@ -0,0 +1,4 @@
+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
diff --git a/tool.gif b/tool.gif
new file mode 100644 (file)
index 0000000..8aa90b5
Binary files /dev/null and b/tool.gif differ
diff --git a/version.txt b/version.txt
new file mode 100644 (file)
index 0000000..c5ddf26
--- /dev/null
@@ -0,0 +1 @@
+3.55.1
diff --git a/www/GRUFGroups.gif b/www/GRUFGroups.gif
new file mode 100644 (file)
index 0000000..6a7fb9f
Binary files /dev/null and b/www/GRUFGroups.gif differ
diff --git a/www/GRUFUsers.gif b/www/GRUFUsers.gif
new file mode 100644 (file)
index 0000000..cc199e6
Binary files /dev/null and b/www/GRUFUsers.gif differ
diff --git a/www/GroupUserFolder.gif b/www/GroupUserFolder.gif
new file mode 100644 (file)
index 0000000..cbfdef2
Binary files /dev/null and b/www/GroupUserFolder.gif differ
diff --git a/www/LDAPGroupFolder.gif b/www/LDAPGroupFolder.gif
new file mode 100644 (file)
index 0000000..cbb71a4
Binary files /dev/null and b/www/LDAPGroupFolder.gif differ
diff --git a/www/down_arrow.gif b/www/down_arrow.gif
new file mode 100644 (file)
index 0000000..f8da0c6
Binary files /dev/null and b/www/down_arrow.gif differ
diff --git a/www/down_arrow_grey.gif b/www/down_arrow_grey.gif
new file mode 100644 (file)
index 0000000..5e90141
Binary files /dev/null and b/www/down_arrow_grey.gif differ
diff --git a/www/up_arrow.gif b/www/up_arrow.gif
new file mode 100644 (file)
index 0000000..7ed42e2
Binary files /dev/null and b/www/up_arrow.gif differ
diff --git a/www/up_arrow_grey.gif b/www/up_arrow_grey.gif
new file mode 100644 (file)
index 0000000..7679aa4
Binary files /dev/null and b/www/up_arrow_grey.gif differ