From 8a8ab8d28efdd32e4316ea8d9242a36a594379cc Mon Sep 17 00:00:00 2001 From: =?utf8?q?Beno=C3=AEt=20Pin?= <benoit.pin@gmail.com> Date: Mon, 25 Oct 2010 12:58:48 +0200 Subject: [PATCH] =?utf8?q?Ajout=20de=20GroupUserFolder=20extrait=20de=20l'?= =?utf8?q?=C3=A9tat=20suivant=20:=20URL:=20http://svn.cri.ensmp.fr/svn/Gro?= =?utf8?q?upUserFolder/branches/3.55.1=20Repository=20Root:=20http://svn.c?= =?utf8?q?ri.ensmp.fr/svn/GroupUserFolder=20Repository=20UUID:=201bf790b2-?= =?utf8?q?e4d4-0310-9a6c-c8a412c25dae=20Revision:=20591=20Node=20Kind:=20d?= =?utf8?q?irectory=20Schedule:=20normal=20Last=20Changed=20Author:=20pin?= =?utf8?q?=20Last=20Changed=20Rev:=20591=20Last=20Changed=20Date:=202009-0?= =?utf8?q?2-11=2018:49:59=20+0100=20(Mer,=2011=20f=C3=A9v=202009)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- ABOUT | 1 + CHANGES | 375 +++ CONTRIBUTORS | 20 + DynaList.py | 96 + Extensions/Install.py | 126 + Extensions/__init__.py | 24 + GRUFFolder.py | 299 ++ GRUFUser.py | 935 ++++++ GroupDataTool.py | 443 +++ GroupUserFolder.py | 2806 +++++++++++++++++ GroupsTool.py | 495 +++ GroupsToolPermissions.py | 49 + INSTALL.txt | 16 + Installation.py | 247 ++ LDAPGroupFolder.py | 393 +++ LDAPUserFolderAdapter.py | 215 ++ LICENSE | 57 + LICENSE.txt | 57 + Log.py | 195 ++ PRODUCT_NAME | 1 + PatchCatalogTool.py | 23 + PloneFeaturePreview.py | 271 ++ README.txt | 118 + TESTED_WITH | 21 + TODO | 82 + __init__.py | 128 + class_utility.py | 197 ++ cvs2cl.pl | 1995 ++++++++++++ debian/changelog | 6 + debian/config | 22 + debian/control | 22 + debian/copyright | 18 + debian/postinst | 50 + debian/prerm | 40 + debian/rules | 89 + debian/templates | 7 + debian/watch | 5 + design.txt | 34 + doc/FAQ | 43 + doc/GRUF3.0.stx | 80 + doc/GRUFLogo.png | Bin 0 -> 31213 bytes doc/SCREENSHOTS | 32 + doc/folder_contents.png | Bin 0 -> 22877 bytes doc/icon.png | Bin 0 -> 31213 bytes doc/interview.txt | 111 + doc/menu.png | Bin 0 -> 62208 bytes doc/tab_audit.png | Bin 0 -> 25238 bytes doc/tab_groups.png | Bin 0 -> 31957 bytes doc/tab_overview.png | Bin 0 -> 30494 bytes doc/tab_sources.png | Bin 0 -> 23197 bytes doc/tab_users.png | Bin 0 -> 38818 bytes doc/user_edit.png | Bin 0 -> 69485 bytes dtml/GRUFFolder_main.dtml | 275 ++ dtml/GRUF_audit.zpt | 236 ++ dtml/GRUF_contents.zpt | 216 ++ dtml/GRUF_groups.zpt | 267 ++ dtml/GRUF_newusers.zpt | 32 + dtml/GRUF_overview.zpt | 208 ++ dtml/GRUF_user.zpt | 247 ++ dtml/GRUF_users.zpt | 340 ++ dtml/GRUF_wizard.zpt | 127 + dtml/addLDAPGroupFolder.dtml | 55 + dtml/configureGroupsTool.dtml | 52 + dtml/explainGroupDataTool.dtml | 10 + dtml/explainGroupsTool.dtml | 11 + dtml/groups.dtml | 224 ++ dtml/roles.png | Bin 0 -> 26916 bytes global_symbols.py | 91 + interfaces/.cvsignore | 2 + interfaces/IUserFolder.py | 614 ++++ interfaces/__init__.py | 26 + interfaces/portal_groupdata.py | 93 + interfaces/portal_groups.py | 144 + product.txt | 1 + .../gruf/GroupSpaceFolderishType_view.pt.old | 16 + skins/gruf/change_password.py | 31 + skins/gruf/defaultGroup.gif | Bin 0 -> 1225 bytes skins/gruf/folder_localrole_form_plone1.pt | 358 +++ skins/gruf/getUsersInGroup.py | 21 + skins/gruf/gruf_ldap_required_fields.py | 14 + skins/gruf/prefs_group_manage.cpy | 26 + skins/gruf/prefs_group_manage.cpy.metadata | 6 + skins/gruf_plone_2_0/README.txt | 4 + skins/gruf_plone_2_0/folder_localrole_form.pt | 445 +++ svn-commit.tmp | 4 + tool.gif | Bin 0 -> 166 bytes version.txt | 1 + www/GRUFGroups.gif | Bin 0 -> 607 bytes www/GRUFUsers.gif | Bin 0 -> 539 bytes www/GroupUserFolder.gif | Bin 0 -> 600 bytes www/LDAPGroupFolder.gif | Bin 0 -> 977 bytes www/down_arrow.gif | Bin 0 -> 56 bytes www/down_arrow_grey.gif | Bin 0 -> 56 bytes www/up_arrow.gif | Bin 0 -> 54 bytes www/up_arrow_grey.gif | Bin 0 -> 54 bytes 95 files changed, 14441 insertions(+) create mode 100644 ABOUT create mode 100644 CHANGES create mode 100644 CONTRIBUTORS create mode 100644 DynaList.py create mode 100644 Extensions/Install.py create mode 100644 Extensions/__init__.py create mode 100644 GRUFFolder.py create mode 100644 GRUFUser.py create mode 100644 GroupDataTool.py create mode 100644 GroupUserFolder.py create mode 100644 GroupsTool.py create mode 100644 GroupsToolPermissions.py create mode 100644 INSTALL.txt create mode 100644 Installation.py create mode 100755 LDAPGroupFolder.py create mode 100755 LDAPUserFolderAdapter.py create mode 100644 LICENSE create mode 100644 LICENSE.txt create mode 100644 Log.py create mode 100644 PRODUCT_NAME create mode 100644 PatchCatalogTool.py create mode 100755 PloneFeaturePreview.py create mode 100644 README.txt create mode 100644 TESTED_WITH create mode 100644 TODO create mode 100644 __init__.py create mode 100644 class_utility.py create mode 100755 cvs2cl.pl create mode 100644 debian/changelog create mode 100755 debian/config create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/postinst create mode 100755 debian/prerm create mode 100755 debian/rules create mode 100644 debian/templates create mode 100644 debian/watch create mode 100644 design.txt create mode 100644 doc/FAQ create mode 100644 doc/GRUF3.0.stx create mode 100644 doc/GRUFLogo.png create mode 100644 doc/SCREENSHOTS create mode 100644 doc/folder_contents.png create mode 100644 doc/icon.png create mode 100644 doc/interview.txt create mode 100644 doc/menu.png create mode 100644 doc/tab_audit.png create mode 100644 doc/tab_groups.png create mode 100644 doc/tab_overview.png create mode 100644 doc/tab_sources.png create mode 100644 doc/tab_users.png create mode 100644 doc/user_edit.png create mode 100644 dtml/GRUFFolder_main.dtml create mode 100644 dtml/GRUF_audit.zpt create mode 100644 dtml/GRUF_contents.zpt create mode 100644 dtml/GRUF_groups.zpt create mode 100644 dtml/GRUF_newusers.zpt create mode 100644 dtml/GRUF_overview.zpt create mode 100644 dtml/GRUF_user.zpt create mode 100644 dtml/GRUF_users.zpt create mode 100644 dtml/GRUF_wizard.zpt create mode 100755 dtml/addLDAPGroupFolder.dtml create mode 100644 dtml/configureGroupsTool.dtml create mode 100644 dtml/explainGroupDataTool.dtml create mode 100644 dtml/explainGroupsTool.dtml create mode 100755 dtml/groups.dtml create mode 100644 dtml/roles.png create mode 100644 global_symbols.py create mode 100644 interfaces/.cvsignore create mode 100644 interfaces/IUserFolder.py create mode 100644 interfaces/__init__.py create mode 100644 interfaces/portal_groupdata.py create mode 100644 interfaces/portal_groups.py create mode 100644 product.txt create mode 100644 skins/gruf/GroupSpaceFolderishType_view.pt.old create mode 100644 skins/gruf/change_password.py create mode 100644 skins/gruf/defaultGroup.gif create mode 100644 skins/gruf/folder_localrole_form_plone1.pt create mode 100644 skins/gruf/getUsersInGroup.py create mode 100755 skins/gruf/gruf_ldap_required_fields.py create mode 100755 skins/gruf/prefs_group_manage.cpy create mode 100755 skins/gruf/prefs_group_manage.cpy.metadata create mode 100755 skins/gruf_plone_2_0/README.txt create mode 100644 skins/gruf_plone_2_0/folder_localrole_form.pt create mode 100644 svn-commit.tmp create mode 100644 tool.gif create mode 100644 version.txt create mode 100644 www/GRUFGroups.gif create mode 100644 www/GRUFUsers.gif create mode 100644 www/GroupUserFolder.gif create mode 100644 www/LDAPGroupFolder.gif create mode 100644 www/down_arrow.gif create mode 100644 www/down_arrow_grey.gif create mode 100644 www/up_arrow.gif create mode 100644 www/up_arrow_grey.gif diff --git a/ABOUT b/ABOUT new file mode 100644 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 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 index 0000000..97b6f84 --- /dev/null +++ b/CONTRIBUTORS @@ -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 index 0000000..22b1be6 --- /dev/null +++ b/DynaList.py @@ -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 index 0000000..87de596 --- /dev/null +++ b/Extensions/Install.py @@ -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 index 0000000..9889842 --- /dev/null +++ b/Extensions/__init__.py @@ -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 index 0000000..6e3fef6 --- /dev/null +++ b/GRUFFolder.py @@ -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 index 0000000..4142c42 --- /dev/null +++ b/GRUFUser.py @@ -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 index 0000000..9b05326 --- /dev/null +++ b/GroupDataTool.py @@ -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 index 0000000..8d6d85a --- /dev/null +++ b/GroupUserFolder.py @@ -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 index 0000000..e76caa1 --- /dev/null +++ b/GroupsTool.py @@ -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 index 0000000..bc6b1b6 --- /dev/null +++ b/GroupsToolPermissions.py @@ -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 index 0000000..7373ab1 --- /dev/null +++ b/INSTALL.txt @@ -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 index 0000000..1be3578 --- /dev/null +++ b/Installation.py @@ -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 index 0000000..a269384 --- /dev/null +++ b/LDAPGroupFolder.py @@ -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 index 0000000..642cdb8 --- /dev/null +++ b/LDAPUserFolderAdapter.py @@ -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 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 index 0000000..5fe2d42 --- /dev/null +++ b/LICENSE.txt @@ -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 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 index 0000000..aaad7b8 --- /dev/null +++ b/PRODUCT_NAME @@ -0,0 +1 @@ +GroupUserFolder diff --git a/PatchCatalogTool.py b/PatchCatalogTool.py new file mode 100644 index 0000000..a9d54e9 --- /dev/null +++ b/PatchCatalogTool.py @@ -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 index 0000000..0cf9854 --- /dev/null +++ b/PloneFeaturePreview.py @@ -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 index 0000000..bf9168a --- /dev/null +++ b/README.txt @@ -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 index 0000000..5f4a809 --- /dev/null +++ b/TESTED_WITH @@ -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 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 index 0000000..e7603ac --- /dev/null +++ b/__init__.py @@ -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 index 0000000..79357bd --- /dev/null +++ b/class_utility.py @@ -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 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. + + +use strict; +use Text::Wrap; +use Time::Local; +use File::Basename; + + +# 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. + + +############### 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 ############ + + +&parse_options (); +&derive_change_log (); + + +### Everything below is subroutine definitions. ### + +# If accumulating, grab the boundary date from pre-existing ChangeLog. +sub maybe_grab_accumulation_date () +{ + if (! $Cumulative) { + return ""; + } + + # else + + open (LOG, "$Log_File_Name") + or die ("trouble opening $Log_File_Name for reading ($!)"); + + my $boundary_date; + while (<LOG>) + { + if (/^(\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)/) + { + $boundary_date = "$1"; + last; + } + } + + close (LOG); + return $boundary_date; +} + +# Fills up a ChangeLog structure in the current directory. +sub derive_change_log () +{ + # See "The Plan" above for a full explanation. + + my %grand_poobah; + + my $file_full_path; + my $time; + my $revision; + my $author; + my $msg_txt; + my $detected_file_separator; + + my %tag_date_printed; + + # Might be adding to an existing ChangeLog + my $accumulation_date = &maybe_grab_accumulation_date (); + if ($accumulation_date) { + # Insert -d immediately after 'cvs log' + my $Log_Date_Command = "-d\'>${accumulation_date}\'"; + $Log_Source_Command =~ s/(^.*log\S*)/$1 $Log_Date_Command/; + &debug ("(adding log msg starting from $accumulation_date)\n"); + } + + # We might be expanding usernames + my %usermap; + + # In general, it's probably not very maintainable to use state + # variables like this to tell the loop what it's doing at any given + # moment, but this is only the first one, and if we never have more + # than a few of these, it's okay. + my $collecting_symbolic_names = 0; + my %symbolic_names; # Where tag names get stored. + my %branch_names; # We'll grab branch names while we're at it. + my %branch_numbers; # Save some revisions for @Follow_Branches + my @branch_roots; # For showing which files are branch ancestors. + + # Bleargh. Compensate for a deficiency of custom wrapping. + if (($After_Header ne " ") and $FSF_Style) + { + $After_Header .= "\t"; + } + + if (! $Input_From_Stdin) { + &debug ("(run \"${Log_Source_Command}\")\n"); + open (LOG_SOURCE, "$Log_Source_Command |") + or die "unable to run \"${Log_Source_Command}\""; + } + else { + open (LOG_SOURCE, "-") or die "unable to open stdin for reading"; + } + + binmode LOG_SOURCE; + + %usermap = &maybe_read_user_map_file (); + + while (<LOG_SOURCE>) + { + # Canonicalize line endings + s/\r$//; + # If on a new file and don't see filename, skip until we find it, and + # when we find it, grab it. + if ((! (defined $file_full_path)) and /^Working file: (.*)/) + { + $file_full_path = $1; + if (@Ignore_Files) + { + my $base; + ($base, undef, undef) = fileparse ($file_full_path); + # Ouch, I wish trailing operators in regexps could be + # evaluated on the fly! + if ($Case_Insensitive) { + if (grep ($file_full_path =~ m|$_|i, @Ignore_Files)) { + undef $file_full_path; + } + } + elsif (grep ($file_full_path =~ m|$_|, @Ignore_Files)) { + undef $file_full_path; + } + } + next; + } + + # Just spin wheels if no file defined yet. + next if (! $file_full_path); + + # Collect tag names in case we're asked to print them in the output. + if (/^symbolic names:$/) { + $collecting_symbolic_names = 1; + next; # There's no more info on this line, so skip to next + } + if ($collecting_symbolic_names) + { + # All tag names are listed with whitespace in front in cvs log + # output; so if see non-whitespace, then we're done collecting. + if (/^\S/) { + $collecting_symbolic_names = 0; + } + else # we're looking at a tag name, so parse & store it + { + # According to the Cederqvist manual, in node "Tags", tag + # names must start with an uppercase or lowercase letter and + # can contain uppercase and lowercase letters, digits, `-', + # and `_'. However, it's not our place to enforce that, so + # we'll allow anything CVS hands us to be a tag: + /^\s+([^:]+): ([\d.]+)$/; + my $tag_name = $1; + my $tag_rev = $2; + + # A branch number either has an odd number of digit sections + # (and hence an even number of dots), or has ".0." as the + # second-to-last digit section. Test for these conditions. + my $real_branch_rev = ""; + if (($tag_rev =~ /^(\d+\.\d+\.)+\d+$/) # Even number of dots... + and (! ($tag_rev =~ /^(1\.)+1$/))) # ...but not "1.[1.]1" + { + $real_branch_rev = $tag_rev; + } + elsif ($tag_rev =~ /(\d+\.(\d+\.)+)0.(\d+)/) # Has ".0." + { + $real_branch_rev = $1 . $3; + } + # If we got a branch, record its number. + if ($real_branch_rev) + { + $branch_names{$real_branch_rev} = $tag_name; + if (@Follow_Branches) { + if (grep ($_ eq $tag_name, @Follow_Branches)) { + $branch_numbers{$tag_name} = $real_branch_rev; + } + } + } + else { + # Else it's just a regular (non-branch) tag. + push (@{$symbolic_names{$tag_rev}}, $tag_name); + } + } + } + # End of code for collecting tag names. + + # If have file name, but not revision, and see revision, then grab + # it. (We collect unconditionally, even though we may or may not + # ever use it.) + if ((! (defined $revision)) and (/^revision (\d+\.[\d.]+)/)) + { + $revision = $1; + + if (@Follow_Branches) + { + foreach my $branch (@Follow_Branches) + { + # Special case for following trunk revisions + if (($branch =~ /^trunk$/i) and ($revision =~ /^[0-9]+\.[0-9]+$/)) + { + goto dengo; + } + + my $branch_number = $branch_numbers{$branch}; + if ($branch_number) + { + # Are we on one of the follow branches or an ancestor of + # same? + # + # If this revision is a prefix of the branch number, or + # possibly is less in the minormost number, OR if this + # branch number is a prefix of the revision, then yes. + # Otherwise, no. + # + # So below, we determine if any of those conditions are + # met. + + # Trivial case: is this revision on the branch? + # (Compare this way to avoid regexps that screw up Emacs + # indentation, argh.) + if ((substr ($revision, 0, ((length ($branch_number)) + 1))) + eq ($branch_number . ".")) + { + goto dengo; + } + # Non-trivial case: check if rev is ancestral to branch + elsif ((length ($branch_number)) > (length ($revision))) + { + $revision =~ /^((?:\d+\.)+)(\d+)$/; + my $r_left = $1; # still has the trailing "." + my $r_end = $2; + + $branch_number =~ /^((?:\d+\.)+)(\d+)\.\d+$/; + my $b_left = $1; # still has trailing "." + my $b_mid = $2; # has no trailing "." + + if (($r_left eq $b_left) + && ($r_end <= $b_mid)) + { + goto dengo; + } + } + } + } + } + else # (! @Follow_Branches) + { + next; + } + + # Else we are following branches, but this revision isn't on the + # path. So skip it. + undef $revision; + dengo: + next; + } + + # If we don't have a revision right now, we couldn't possibly + # be looking at anything useful. + if (! (defined ($revision))) { + $detected_file_separator = /^$file_separator$/o; + if ($detected_file_separator) { + # No revisions for this file; can happen, e.g. "cvs log -d DATE" + goto CLEAR; + } + else { + next; + } + } + + # If have file name but not date and author, and see date or + # author, then grab them: + unless (defined $time) + { + if (/^date: .*/) + { + ($time, $author) = &parse_date_and_author ($_); + if (defined ($usermap{$author}) and $usermap{$author}) { + $author = $usermap{$author}; + } + } + else { + $detected_file_separator = /^$file_separator$/o; + if ($detected_file_separator) { + # No revisions for this file; can happen, e.g. "cvs log -d DATE" + goto CLEAR; + } + } + # If the date/time/author hasn't been found yet, we couldn't + # possibly care about anything we see. So skip: + next; + } + + # A "branches: ..." line here indicates that one or more branches + # are rooted at this revision. If we're showing branches, then we + # want to show that fact as well, so we collect all the branches + # that this is the latest ancestor of and store them in + # @branch_roots. Just for reference, the format of the line we're + # seeing at this point is: + # + # branches: 1.5.2; 1.5.4; ...; + # + # Okay, here goes: + + if (/^branches:\s+(.*);$/) + { + if ($Show_Branches) + { + my $lst = $1; + $lst =~ s/(1\.)+1;|(1\.)+1$//; # ignore the trivial branch 1.1.1 + if ($lst) { + @branch_roots = split (/;\s+/, $lst); + } + else { + undef @branch_roots; + } + next; + } + else + { + # Ugh. This really bothers me. Suppose we see a log entry + # like this: + # + # ---------------------------- + # revision 1.1 + # date: 1999/10/17 03:07:38; author: jrandom; state: Exp; + # branches: 1.1.2; + # Intended first line of log message begins here. + # ---------------------------- + # + # The question is, how we can tell the difference between that + # log message and a *two*-line log message whose first line is + # + # "branches: 1.1.2;" + # + # See the problem? The output of "cvs log" is inherently + # ambiguous. + # + # For now, we punt: we liberally assume that people don't + # write log messages like that, and just toss a "branches:" + # line if we see it but are not showing branches. I hope no + # one ever loses real log data because of this. + next; + } + } + + # If have file name, time, and author, then we're just grabbing + # log message texts: + $detected_file_separator = /^$file_separator$/o; + if ($detected_file_separator && ! (defined $revision)) { + # No revisions for this file; can happen, e.g. "cvs log -d DATE" + goto CLEAR; + } + unless ($detected_file_separator || /^$logmsg_separator$/o) + { + $msg_txt .= $_; # Normally, just accumulate the message... + next; + } + # ... until a msg separator is encountered: + # Ensure the message contains something: + if ((! $msg_txt) + || ($msg_txt =~ /^\s*\.\s*$|^\s*$/) + || ($msg_txt =~ /\*\*\* empty log message \*\*\*/)) + { + if ($Prune_Empty_Msgs) { + goto CLEAR; + } + # else + $msg_txt = "[no log message]\n"; + } + + ### Store it all in the Grand Poobah: + { + my $dir_key; # key into %grand_poobah + my %qunk; # complicated little jobbie, see below + + # Each revision of a file has a little data structure (a `qunk') + # associated with it. That data structure holds not only the + # file's name, but any additional information about the file + # that might be needed in the output, such as the revision + # number, tags, branches, etc. The reason to have these things + # arranged in a data structure, instead of just appending them + # textually to the file's name, is that we may want to do a + # little rearranging later as we write the output. For example, + # all the files on a given tag/branch will go together, followed + # by the tag in parentheses (so trunk or otherwise non-tagged + # files would go at the end of the file list for a given log + # message). This rearrangement is a lot easier to do if we + # don't have to reparse the text. + # + # A qunk looks like this: + # + # { + # filename => "hello.c", + # revision => "1.4.3.2", + # time => a timegm() return value (moment of commit) + # tags => [ "tag1", "tag2", ... ], + # branch => "branchname" # There should be only one, right? + # branchroots => [ "branchtag1", "branchtag2", ... ] + # } + + if ($Distributed) { + # Just the basename, don't include the path. + ($qunk{'filename'}, $dir_key, undef) = fileparse ($file_full_path); + } + else { + $dir_key = "./"; + $qunk{'filename'} = $file_full_path; + } + + # This may someday be used in a more sophisticated calculation + # of what other files are involved in this commit. For now, we + # don't use it much except for delta mode, because the + # common-commit-detection algorithm is hypothesized to be + # "good enough" as it stands. + $qunk{'time'} = $time; + + # We might be including revision numbers and/or tags and/or + # branch names in the output. Most of the code from here to + # loop-end deals with organizing these in qunk. + + $qunk{'revision'} = $revision; + + # Grab the branch, even though we may or may not need it: + $qunk{'revision'} =~ /((?:\d+\.)+)\d+/; + my $branch_prefix = $1; + $branch_prefix =~ s/\.$//; # strip off final dot + if ($branch_names{$branch_prefix}) { + $qunk{'branch'} = $branch_names{$branch_prefix}; + } + + # If there's anything in the @branch_roots array, then this + # revision is the root of at least one branch. We'll display + # them as branch names instead of revision numbers, the + # substitution for which is done directly in the array: + if (@branch_roots) { + my @roots = map { $branch_names{$_} } @branch_roots; + $qunk{'branchroots'} = \@roots; + } + + # Save tags too. + if (defined ($symbolic_names{$revision})) { + $qunk{'tags'} = $symbolic_names{$revision}; + delete $symbolic_names{$revision}; + + # If we're in 'delta' mode, update the latest observed + # times for the beginning and ending tags, and + # when we get around to printing output, we will simply restrict + # ourselves to that timeframe... + + if ($Delta_Mode) { + if (($time > $Delta_StartTime) && + (grep { $_ eq $Delta_From } @{$qunk{'tags'}})) + { + $Delta_StartTime = $time; + } + + if (($time > $Delta_EndTime) && + (grep { $_ eq $Delta_To } @{$qunk{'tags'}})) + { + $Delta_EndTime = $time; + } + } + } + + # Add this file to the list + # (We use many spoonfuls of autovivication magic. Hashes and arrays + # will spring into existence if they aren't there already.) + + &debug ("(pushing log msg for ${dir_key}$qunk{'filename'})\n"); + + # Store with the files in this commit. Later we'll loop through + # again, making sure that revisions with the same log message + # and nearby commit times are grouped together as one commit. + push (@{$grand_poobah{$dir_key}{$author}{$time}{$msg_txt}}, \%qunk); + } + + CLEAR: + # Make way for the next message + undef $msg_txt; + undef $time; + undef $revision; + undef $author; + undef @branch_roots; + + # Maybe even make way for the next file: + if ($detected_file_separator) { + undef $file_full_path; + undef %branch_names; + undef %branch_numbers; + undef %symbolic_names; + } + } + + close (LOG_SOURCE); + + ### Process each ChangeLog + + while (my ($dir,$authorhash) = each %grand_poobah) + { + &debug ("DOING DIR: $dir\n"); + + # Here we twist our hash around, from being + # author => time => message => filelist + # in %$authorhash to + # time => author => message => filelist + # in %changelog. + # + # This is also where we merge entries. The algorithm proceeds + # through the timeline of the changelog with a sliding window of + # $Max_Checkin_Duration seconds; within that window, entries that + # have the same log message are merged. + # + # (To save space, we zap %$authorhash after we've copied + # everything out of it.) + + my %changelog; + while (my ($author,$timehash) = each %$authorhash) + { + my $lasttime; + my %stamptime; + foreach my $time (sort {$main::a <=> $main::b} (keys %$timehash)) + { + my $msghash = $timehash->{$time}; + while (my ($msg,$qunklist) = each %$msghash) + { + my $stamptime = $stamptime{$msg}; + if ((defined $stamptime) + and (($time - $stamptime) < $Max_Checkin_Duration) + and (defined $changelog{$stamptime}{$author}{$msg})) + { + push(@{$changelog{$stamptime}{$author}{$msg}}, @$qunklist); + } + else { + $changelog{$time}{$author}{$msg} = $qunklist; + $stamptime{$msg} = $time; + } + } + } + } + undef (%$authorhash); + + ### Now we can write out the ChangeLog! + + my ($logfile_here, $logfile_bak, $tmpfile); + + if (! $Output_To_Stdout) { + $logfile_here = $dir . $Log_File_Name; + $logfile_here =~ s/^\.\/\//\//; # fix any leading ".//" problem + $tmpfile = "${logfile_here}.cvs2cl$$.tmp"; + $logfile_bak = "${logfile_here}.bak"; + + open (LOG_OUT, ">$tmpfile") or die "Unable to open \"$tmpfile\""; + } + else { + open (LOG_OUT, ">-") or die "Unable to open stdout for writing"; + } + + print LOG_OUT $ChangeLog_Header; + + if ($XML_Output) { + my $encoding = + length $XML_Encoding ? qq'encoding="$XML_Encoding"' : ''; + my $version = 'version="1.0"'; + my $declaration = + sprintf '<?xml %s?>', join ' ', grep length, $version, $encoding; + my $root = + '<changelog xmlns="http://www.red-bean.com/xmlns/cvs2cl/">'; + print LOG_OUT "$declaration\n\n$root\n\n"; + } + + foreach my $time (sort {$main::b <=> $main::a} (keys %changelog)) + { + next if ($Delta_Mode && + (($time <= $Delta_StartTime) || + ($time > $Delta_EndTime && $Delta_EndTime))); + + # Set up the date/author line. + # kff todo: do some more XML munging here, on the header + # part of the entry: + my ($ignore,$min,$hour,$mday,$mon,$year,$wday) + = $UTC_Times ? gmtime($time) : localtime($time); + + # XML output includes everything else, we might as well make + # it always include Day Of Week too, for consistency. + if ($Show_Day_Of_Week or $XML_Output) { + $wday = ("Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday")[$wday]; + $wday = ($XML_Output) ? "<weekday>${wday}</weekday>\n" : " $wday"; + } + else { + $wday = ""; + } + + my $authorhash = $changelog{$time}; + if ($Show_Tag_Dates) { + my %tags; + while (my ($author,$mesghash) = each %$authorhash) { + while (my ($msg,$qunk) = each %$mesghash) { + foreach my $qunkref2 (@$qunk) { + if (defined ($$qunkref2{'tags'})) { + foreach my $tag (@{$$qunkref2{'tags'}}) { + $tags{$tag} = 1; + } + } + } + } + } + foreach my $tag (keys %tags) { + if (!defined $tag_date_printed{$tag}) { + $tag_date_printed{$tag} = $time; + if ($XML_Output) { + # NOT YET DONE + } + else { + if ($Show_Times) { + printf LOG_OUT ("%4u-%02u-%02u${wday} %02u:%02u tag %s\n\n", + $year+1900, $mon+1, $mday, $hour, $min, $tag); + } else { + printf LOG_OUT ("%4u-%02u-%02u${wday} tag %s\n\n", + $year+1900, $mon+1, $mday, $tag); + } + } + } + } + } + while (my ($author,$mesghash) = each %$authorhash) + { + # If XML, escape in outer loop to avoid compound quoting: + if ($XML_Output) { + $author = &xml_escape ($author); + } + + FOOBIE: + while (my ($msg,$qunklist) = each %$mesghash) + { + ## MJP: 19.xii.01 : Exclude @ignore_tags + for my $ignore_tag (@ignore_tags) { + next FOOBIE + if grep $_ eq $ignore_tag, map(@{$_->{tags}}, + grep(defined $_->{tags}, + @$qunklist)); + } + ## MJP: 19.xii.01 : End exclude @ignore_tags + + my $files = &pretty_file_list ($qunklist); + my $header_line; # date and author + my $body; # see below + my $wholething; # $header_line + $body + + if ($XML_Output) { + $header_line = + sprintf ("<date>%4u-%02u-%02u</date>\n" + . "${wday}" + . "<time>%02u:%02u</time>\n" + . "<author>%s</author>\n", + $year+1900, $mon+1, $mday, $hour, $min, $author); + } + else { + if ($Show_Times) { + $header_line = + sprintf ("%4u-%02u-%02u${wday} %02u:%02u %s\n\n", + $year+1900, $mon+1, $mday, $hour, $min, $author); + } else { + $header_line = + sprintf ("%4u-%02u-%02u${wday} %s\n\n", + $year+1900, $mon+1, $mday, $author); + } + } + + $Text::Wrap::huge = 'overflow' + if $Text::Wrap::VERSION >= 2001.0130; + # Reshape the body according to user preferences. + if ($XML_Output) + { + $msg = &preprocess_msg_text ($msg); + $body = $files . $msg; + } + elsif ($No_Wrap) + { + $msg = &preprocess_msg_text ($msg); + $files = wrap ("\t", " ", "$files"); + $msg =~ s/\n(.*)/\n\t$1/g; + unless ($After_Header eq " ") { + $msg =~ s/^(.*)/\t$1/g; + } + $body = $files . $After_Header . $msg; + } + else # do wrapping, either FSF-style or regular + { + if ($FSF_Style) + { + $files = wrap ("\t", " ", "$files"); + + my $files_last_line_len = 0; + if ($After_Header eq " ") + { + $files_last_line_len = &last_line_len ($files); + $files_last_line_len += 1; # for $After_Header + } + + $msg = &wrap_log_entry + ($msg, "\t", 69 - $files_last_line_len, 69); + $body = $files . $After_Header . $msg; + } + else # not FSF-style + { + $msg = &preprocess_msg_text ($msg); + $body = $files . $After_Header . $msg; + $body = wrap ("\t", " ", "$body"); + } + } + + $wholething = $header_line . $body; + + if ($XML_Output) { + $wholething = "<entry>\n${wholething}</entry>\n"; + } + + # One last check: make sure it passes the regexp test, if the + # user asked for that. We have to do it here, so that the + # test can match against information in the header as well + # as in the text of the log message. + + # How annoying to duplicate so much code just because I + # can't figure out a way to evaluate scalars on the trailing + # operator portion of a regular expression. Grrr. + if ($Case_Insensitive) { + unless ($Regexp_Gate && ($wholething !~ /$Regexp_Gate/oi)) { + print LOG_OUT "${wholething}\n"; + } + } + else { + unless ($Regexp_Gate && ($wholething !~ /$Regexp_Gate/o)) { + print LOG_OUT "${wholething}\n"; + } + } + } + } + } + + if ($XML_Output) { + print LOG_OUT "</changelog>\n"; + } + + close (LOG_OUT); + + if (! $Output_To_Stdout) + { + # If accumulating, append old data to new before renaming. But + # don't append the most recent entry, since it's already in the + # new log due to CVS's idiosyncratic interpretation of "log -d". + if ($Cumulative && -f $logfile_here) + { + open (NEW_LOG, ">>$tmpfile") + or die "trouble appending to $tmpfile ($!)"; + + open (OLD_LOG, "<$logfile_here") + or die "trouble reading from $logfile_here ($!)"; + + my $started_first_entry = 0; + my $passed_first_entry = 0; + while (<OLD_LOG>) + { + if (! $passed_first_entry) + { + if ((! $started_first_entry) + && /^(\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)/) { + $started_first_entry = 1; + } + elsif (/^(\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)/) { + $passed_first_entry = 1; + print NEW_LOG $_; + } + } + else { + print NEW_LOG $_; + } + } + + close (NEW_LOG); + close (OLD_LOG); + } + + if (-f $logfile_here) { + rename ($logfile_here, $logfile_bak); + } + rename ($tmpfile, $logfile_here); + } + } +} + +sub parse_date_and_author () +{ + # Parses the date/time and author out of a line like: + # + # date: 1999/02/19 23:29:05; author: apharris; state: Exp; + + my $line = shift; + + my ($year, $mon, $mday, $hours, $min, $secs, $author) = $line =~ + m#(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);\s+author:\s+([^;]+);# + or die "Couldn't parse date ``$line''"; + die "Bad date or Y2K issues" unless ($year > 1969 and $year < 2258); + # Kinda arbitrary, but useful as a sanity check + my $time = timegm($secs,$min,$hours,$mday,$mon-1,$year-1900); + + return ($time, $author); +} + +# Here we take a bunch of qunks and convert them into printed +# summary that will include all the information the user asked for. +sub pretty_file_list () +{ + if ($Hide_Filenames and (! $XML_Output)) { + return ""; + } + + my $qunksref = shift; + my @qunkrefs = @$qunksref; + my @filenames; + my $beauty = ""; # The accumulating header string for this entry. + my %non_unanimous_tags; # Tags found in a proper subset of qunks + my %unanimous_tags; # Tags found in all qunks + my %all_branches; # Branches found in any qunk + my $common_dir = undef; # Dir prefix common to all files ("" if none) + my $fbegun = 0; # Did we begin printing filenames yet? + + # First, loop over the qunks gathering all the tag/branch names. + # We'll put them all in non_unanimous_tags, and take out the + # unanimous ones later. + QUNKREF: + foreach my $qunkref (@qunkrefs) + { + ## MJP: 19.xii.01 : Exclude @ignore_tags + for my $ignore_tag (@ignore_tags) { + next QUNKREF + if grep $_ eq $ignore_tag, @{$$qunkref{'tags'}}; + } + ## MJP: 19.xii.01 : End exclude @ignore_tags + + # Keep track of whether all the files in this commit were in the + # same directory, and memorize it if so. We can make the output a + # little more compact by mentioning the directory only once. + if ((scalar (@qunkrefs)) > 1) + { + if (! (defined ($common_dir))) + { + my ($base, $dir); + ($base, $dir, undef) = fileparse ($$qunkref{'filename'}); + + if ((! (defined ($dir))) # this first case is sheer paranoia + or ($dir eq "") + or ($dir eq "./") + or ($dir eq ".\\")) + { + $common_dir = ""; + } + else + { + $common_dir = $dir; + } + } + elsif ($common_dir ne "") + { + # Already have a common dir prefix, so how much of it can we preserve? + $common_dir = &common_path_prefix ($$qunkref{'filename'}, $common_dir); + } + } + else # only one file in this entry anyway, so common dir not an issue + { + $common_dir = ""; + } + + if (defined ($$qunkref{'branch'})) { + $all_branches{$$qunkref{'branch'}} = 1; + } + if (defined ($$qunkref{'tags'})) { + foreach my $tag (@{$$qunkref{'tags'}}) { + $non_unanimous_tags{$tag} = 1; + } + } + } + + # Any tag held by all qunks will be printed specially... but only if + # there are multiple qunks in the first place! + if ((scalar (@qunkrefs)) > 1) { + foreach my $tag (keys (%non_unanimous_tags)) { + my $everyone_has_this_tag = 1; + foreach my $qunkref (@qunkrefs) { + if ((! (defined ($$qunkref{'tags'}))) + or (! (grep ($_ eq $tag, @{$$qunkref{'tags'}})))) { + $everyone_has_this_tag = 0; + } + } + if ($everyone_has_this_tag) { + $unanimous_tags{$tag} = 1; + delete $non_unanimous_tags{$tag}; + } + } + } + + if ($XML_Output) + { + # If outputting XML, then our task is pretty simple, because we + # don't have to detect common dir, common tags, branch prefixing, + # etc. We just output exactly what we have, and don't worry about + # redundancy or readability. + + foreach my $qunkref (@qunkrefs) + { + my $filename = $$qunkref{'filename'}; + my $revision = $$qunkref{'revision'}; + my $tags = $$qunkref{'tags'}; + my $branch = $$qunkref{'branch'}; + my $branchroots = $$qunkref{'branchroots'}; + + $filename = &xml_escape ($filename); # probably paranoia + $revision = &xml_escape ($revision); # definitely paranoia + + $beauty .= "<file>\n"; + $beauty .= "<name>${filename}</name>\n"; + $beauty .= "<revision>${revision}</revision>\n"; + if ($branch) { + $branch = &xml_escape ($branch); # more paranoia + $beauty .= "<branch>${branch}</branch>\n"; + } + foreach my $tag (@$tags) { + $tag = &xml_escape ($tag); # by now you're used to the paranoia + $beauty .= "<tag>${tag}</tag>\n"; + } + foreach my $root (@$branchroots) { + $root = &xml_escape ($root); # which is good, because it will continue + $beauty .= "<branchroot>${root}</branchroot>\n"; + } + $beauty .= "</file>\n"; + } + + # Theoretically, we could go home now. But as long as we're here, + # let's print out the common_dir and utags, as a convenience to + # the receiver (after all, earlier code calculated that stuff + # anyway, so we might as well take advantage of it). + + if ((scalar (keys (%unanimous_tags))) > 1) { + foreach my $utag ((keys (%unanimous_tags))) { + $utag = &xml_escape ($utag); # the usual paranoia + $beauty .= "<utag>${utag}</utag>\n"; + } + } + if ($common_dir) { + $common_dir = &xml_escape ($common_dir); + $beauty .= "<commondir>${common_dir}</commondir>\n"; + } + + # That's enough for XML, time to go home: + return $beauty; + } + + # Else not XML output, so complexly compactify for chordate + # consumption. At this point we have enough global information + # about all the qunks to organize them non-redundantly for output. + + if ($common_dir) { + # Note that $common_dir still has its trailing slash + $beauty .= "$common_dir: "; + } + + if ($Show_Branches) + { + # For trailing revision numbers. + my @brevisions; + + foreach my $branch (keys (%all_branches)) + { + foreach my $qunkref (@qunkrefs) + { + if ((defined ($$qunkref{'branch'})) + and ($$qunkref{'branch'} eq $branch)) + { + if ($fbegun) { + # kff todo: comma-delimited in XML too? Sure. + $beauty .= ", "; + } + else { + $fbegun = 1; + } + my $fname = substr ($$qunkref{'filename'}, length ($common_dir)); + $beauty .= $fname; + $$qunkref{'printed'} = 1; # Just setting a mark bit, basically + + if ($Show_Tags && (defined @{$$qunkref{'tags'}})) { + my @tags = grep ($non_unanimous_tags{$_}, @{$$qunkref{'tags'}}); + + if (@tags) { + $beauty .= " (tags: "; + $beauty .= join (', ', @tags); + $beauty .= ")"; + } + } + + if ($Show_Revisions) { + # Collect the revision numbers' last components, but don't + # print them -- they'll get printed with the branch name + # later. + $$qunkref{'revision'} =~ /.+\.([\d]+)$/; + push (@brevisions, $1); + + # todo: we're still collecting branch roots, but we're not + # showing them anywhere. If we do show them, it would be + # nifty to just call them revision "0" on a the branch. + # Yeah, that's the ticket. + } + } + } + $beauty .= " ($branch"; + if (@brevisions) { + if ((scalar (@brevisions)) > 1) { + $beauty .= ".["; + $beauty .= (join (',', @brevisions)); + $beauty .= "]"; + } + else { + # Square brackets are spurious here, since there's no range to + # encapsulate + $beauty .= ".$brevisions[0]"; + } + } + $beauty .= ")"; + } + } + + # Okay; any qunks that were done according to branch are taken care + # of, and marked as printed. Now print everyone else. + + foreach my $qunkref (@qunkrefs) + { + next if (defined ($$qunkref{'printed'})); # skip if already printed + + if ($fbegun) { + $beauty .= ", "; + } + else { + $fbegun = 1; + } + $beauty .= substr ($$qunkref{'filename'}, length ($common_dir)); + # todo: Shlomo's change was this: + # $beauty .= substr ($$qunkref{'filename'}, + # (($common_dir eq "./") ? "" : length ($common_dir))); + $$qunkref{'printed'} = 1; # Set a mark bit. + + if ($Show_Revisions || $Show_Tags) + { + my $started_addendum = 0; + + if ($Show_Revisions) { + $started_addendum = 1; + $beauty .= " ("; + $beauty .= "$$qunkref{'revision'}"; + } + if ($Show_Tags && (defined $$qunkref{'tags'})) { + my @tags = grep ($non_unanimous_tags{$_}, @{$$qunkref{'tags'}}); + if ((scalar (@tags)) > 0) { + if ($started_addendum) { + $beauty .= ", "; + } + else { + $beauty .= " (tags: "; + } + $beauty .= join (', ', @tags); + $started_addendum = 1; + } + } + if ($started_addendum) { + $beauty .= ")"; + } + } + } + + # Unanimous tags always come last. + if ($Show_Tags && %unanimous_tags) + { + $beauty .= " (utags: "; + $beauty .= join (', ', sort keys (%unanimous_tags)); + $beauty .= ")"; + } + + # todo: still have to take care of branch_roots? + + $beauty = "* $beauty:"; + + return $beauty; +} + +sub common_path_prefix () +{ + my $path1 = shift; + my $path2 = shift; + + my ($dir1, $dir2); + (undef, $dir1, undef) = fileparse ($path1); + (undef, $dir2, undef) = fileparse ($path2); + + # Transmogrify Windows filenames to look like Unix. + # (It is far more likely that someone is running cvs2cl.pl under + # Windows than that they would genuinely have backslashes in their + # filenames.) + $dir1 =~ tr#\\#/#; + $dir2 =~ tr#\\#/#; + + my $accum1 = ""; + my $accum2 = ""; + my $last_common_prefix = ""; + + while ($accum1 eq $accum2) + { + $last_common_prefix = $accum1; + last if ($accum1 eq $dir1); + my ($tmp1) = split (/\//, (substr ($dir1, length ($accum1)))); + my ($tmp2) = split (/\//, (substr ($dir2, length ($accum2)))); + $accum1 .= "$tmp1/" if (defined $tmp1 and $tmp1 ne ''); + $accum2 .= "$tmp2/" if (defined $tmp2 and $tmp2 ne ''); + } + + return $last_common_prefix; +} + +sub preprocess_msg_text () +{ + my $text = shift; + + # Strip out carriage returns (as they probably result from DOSsy editors). + $text =~ s/\r\n/\n/g; + + # If it *looks* like two newlines, make it *be* two newlines: + $text =~ s/\n\s*\n/\n\n/g; + + if ($XML_Output) + { + $text = &xml_escape ($text); + $text = "<msg>${text}</msg>\n"; + } + elsif (! $No_Wrap) + { + # Strip off lone newlines, but only for lines that don't begin with + # whitespace or a mail-quoting character, since we want to preserve + # that kind of formatting. Also don't strip newlines that follow a + # period; we handle those specially next. And don't strip + # newlines that precede an open paren. + 1 while ($text =~ s/(^|\n)([^>\s].*[^.\n])\n([^>\n])/$1$2 $3/g); + + # If a newline follows a period, make sure that when we bring up the + # bottom sentence, it begins with two spaces. + 1 while ($text =~ s/(^|\n)([^>\s].*)\n([^>\n])/$1$2 $3/g); + } + + return $text; +} + +sub last_line_len () +{ + my $files_list = shift; + my @lines = split (/\n/, $files_list); + my $last_line = pop (@lines); + return length ($last_line); +} + +# A custom wrap function, sensitive to some common constructs used in +# log entries. +sub wrap_log_entry () +{ + my $text = shift; # The text to wrap. + my $left_pad_str = shift; # String to pad with on the left. + + # These do NOT take left_pad_str into account: + my $length_remaining = shift; # Amount left on current line. + my $max_line_length = shift; # Amount left for a blank line. + + my $wrapped_text = ""; # The accumulating wrapped entry. + my $user_indent = ""; # Inherited user_indent from prev line. + + my $first_time = 1; # First iteration of the loop? + my $suppress_line_start_match = 0; # Set to disable line start checks. + + my @lines = split (/\n/, $text); + while (@lines) # Don't use `foreach' here, it won't work. + { + my $this_line = shift (@lines); + chomp $this_line; + + if ($this_line =~ /^(\s+)/) { + $user_indent = $1; + } + else { + $user_indent = ""; + } + + # If it matches any of the line-start regexps, print a newline now... + if ($suppress_line_start_match) + { + $suppress_line_start_match = 0; + } + elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/) + || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/) + || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/) + || ($this_line =~ /^(\s+)(\S+)/) + || ($this_line =~ /^(\s*)- +/) + || ($this_line =~ /^()\s*$/) + || ($this_line =~ /^(\s*)\*\) +/) + || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/)) + { + # Make a line break immediately, unless header separator is set + # and this line is the first line in the entry, in which case + # we're getting the blank line for free already and shouldn't + # add an extra one. + unless (($After_Header ne " ") and ($first_time)) + { + if ($this_line =~ /^()\s*$/) { + $suppress_line_start_match = 1; + $wrapped_text .= "\n${left_pad_str}"; + } + + $wrapped_text .= "\n${left_pad_str}"; + } + + $length_remaining = $max_line_length - (length ($user_indent)); + } + + # Now that any user_indent has been preserved, strip off leading + # whitespace, so up-folding has no ugly side-effects. + $this_line =~ s/^\s*//; + + # Accumulate the line, and adjust parameters for next line. + my $this_len = length ($this_line); + if ($this_len == 0) + { + # Blank lines should cancel any user_indent level. + $user_indent = ""; + $length_remaining = $max_line_length; + } + elsif ($this_len >= $length_remaining) # Line too long, try breaking it. + { + # Walk backwards from the end. At first acceptable spot, break + # a new line. + my $idx = $length_remaining - 1; + if ($idx < 0) { $idx = 0 }; + while ($idx > 0) + { + if (substr ($this_line, $idx, 1) =~ /\s/) + { + my $line_now = substr ($this_line, 0, $idx); + my $next_line = substr ($this_line, $idx); + $this_line = $line_now; + + # Clean whitespace off the end. + chomp $this_line; + + # The current line is ready to be printed. + $this_line .= "\n${left_pad_str}"; + + # Make sure the next line is allowed full room. + $length_remaining = $max_line_length - (length ($user_indent)); + + # Strip next_line, but then preserve any user_indent. + $next_line =~ s/^\s*//; + + # Sneak a peek at the user_indent of the upcoming line, so + # $next_line (which will now precede it) can inherit that + # indent level. Otherwise, use whatever user_indent level + # we currently have, which might be none. + my $next_next_line = shift (@lines); + if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) { + $next_line = $1 . $next_line if (defined ($1)); + # $length_remaining = $max_line_length - (length ($1)); + $next_next_line =~ s/^\s*//; + } + else { + $next_line = $user_indent . $next_line; + } + if (defined ($next_next_line)) { + unshift (@lines, $next_next_line); + } + unshift (@lines, $next_line); + + # Our new next line might, coincidentally, begin with one of + # the line-start regexps, so we temporarily turn off + # sensitivity to that until we're past the line. + $suppress_line_start_match = 1; + + last; + } + else + { + $idx--; + } + } + + if ($idx == 0) + { + # We bottomed out because the line is longer than the + # available space. But that could be because the space is + # small, or because the line is longer than even the maximum + # possible space. Handle both cases below. + + if ($length_remaining == ($max_line_length - (length ($user_indent)))) + { + # The line is simply too long -- there is no hope of ever + # breaking it nicely, so just insert it verbatim, with + # appropriate padding. + $this_line = "\n${left_pad_str}${this_line}"; + } + else + { + # Can't break it here, but may be able to on the next round... + unshift (@lines, $this_line); + $length_remaining = $max_line_length - (length ($user_indent)); + $this_line = "\n${left_pad_str}"; + } + } + } + else # $this_len < $length_remaining, so tack on what we can. + { + # Leave a note for the next iteration. + $length_remaining = $length_remaining - $this_len; + + if ($this_line =~ /\.$/) + { + $this_line .= " "; + $length_remaining -= 2; + } + else # not a sentence end + { + $this_line .= " "; + $length_remaining -= 1; + } + } + + # Unconditionally indicate that loop has run at least once. + $first_time = 0; + + $wrapped_text .= "${user_indent}${this_line}"; + } + + # One last bit of padding. + $wrapped_text .= "\n"; + + return $wrapped_text; +} + +sub xml_escape () +{ + my $txt = shift; + $txt =~ s/&/&/g; + $txt =~ s/</</g; + $txt =~ s/>/>/g; + return $txt; +} + +sub maybe_read_user_map_file () +{ + my %expansions; + + if ($User_Map_File) + { + open (MAPFILE, "<$User_Map_File") + or die ("Unable to open $User_Map_File ($!)"); + + while (<MAPFILE>) + { + next if /^\s*#/; # Skip comment lines. + next if not /:/; # Skip lines without colons. + + # It is now safe to split on ':'. + my ($username, $expansion) = split ':'; + chomp $expansion; + $expansion =~ s/^'(.*)'$/$1/; + $expansion =~ s/^"(.*)"$/$1/; + + # If it looks like the expansion has a real name already, then + # we toss the username we got from CVS log. Otherwise, keep + # it to use in combination with the email address. + + if ($expansion =~ /^\s*<{0,1}\S+@.*/) { + # Also, add angle brackets if none present + if (! ($expansion =~ /<\S+@\S+>/)) { + $expansions{$username} = "$username <$expansion>"; + } + else { + $expansions{$username} = "$username $expansion"; + } + } + else { + $expansions{$username} = $expansion; + } + } + + close (MAPFILE); + } + + return %expansions; +} + +sub parse_options () +{ + # Check this internally before setting the global variable. + my $output_file; + + # If this gets set, we encountered unknown options and will exit at + # the end of this subroutine. + my $exit_with_admonishment = 0; + + while (my $arg = shift (@ARGV)) + { + if ($arg =~ /^-h$|^-help$|^--help$|^--usage$|^-?$/) { + $Print_Usage = 1; + } + elsif ($arg =~ /^--delta$/) { + my $narg = shift(@ARGV) || die "$arg needs argument.\n"; + if ($narg =~ /^([A-Za-z][A-Za-z0-9_\-]*):([A-Za-z][A-Za-z0-9_\-]*)$/) { + $Delta_From = $1; + $Delta_To = $2; + $Delta_Mode = 1; + } else { + die "--delta FROM_TAG:TO_TAG is what you meant to say.\n"; + } + } + elsif ($arg =~ /^--debug$/) { # unadvertised option, heh + $Debug = 1; + } + elsif ($arg =~ /^--version$/) { + $Print_Version = 1; + } + elsif ($arg =~ /^-g$|^--global-opts$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + # Don't assume CVS is called "cvs" on the user's system: + $Log_Source_Command =~ s/(^\S*)/$1 $narg/; + } + elsif ($arg =~ /^-l$|^--log-opts$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + $Log_Source_Command .= " $narg"; + } + elsif ($arg =~ /^-f$|^--file$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + $output_file = $narg; + } + elsif ($arg =~ /^--accum$/) { + $Cumulative = 1; + } + elsif ($arg =~ /^--fsf$/) { + $FSF_Style = 1; + } + elsif ($arg =~ /^-U$|^--usermap$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + $User_Map_File = $narg; + } + elsif ($arg =~ /^-W$|^--window$/) { + defined(my $narg = shift (@ARGV)) || die "$arg needs argument.\n"; + $Max_Checkin_Duration = $narg; + } + elsif ($arg =~ /^-I$|^--ignore$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + push (@Ignore_Files, $narg); + } + elsif ($arg =~ /^-C$|^--case-insensitive$/) { + $Case_Insensitive = 1; + } + elsif ($arg =~ /^-R$|^--regexp$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + $Regexp_Gate = $narg; + } + elsif ($arg =~ /^--stdout$/) { + $Output_To_Stdout = 1; + } + elsif ($arg =~ /^--version$/) { + $Print_Version = 1; + } + elsif ($arg =~ /^-d$|^--distributed$/) { + $Distributed = 1; + } + elsif ($arg =~ /^-P$|^--prune$/) { + $Prune_Empty_Msgs = 1; + } + elsif ($arg =~ /^-S$|^--separate-header$/) { + $After_Header = "\n\n"; + } + elsif ($arg =~ /^--no-wrap$/) { + $No_Wrap = 1; + } + elsif ($arg =~ /^--gmt$|^--utc$/) { + $UTC_Times = 1; + } + elsif ($arg =~ /^-w$|^--day-of-week$/) { + $Show_Day_Of_Week = 1; + } + elsif ($arg =~ /^--no-times$/) { + $Show_Times = 0; + } + elsif ($arg =~ /^-r$|^--revisions$/) { + $Show_Revisions = 1; + } + elsif ($arg =~ /^-t$|^--tags$/) { + $Show_Tags = 1; + } + elsif ($arg =~ /^-T$|^--tagdates$/) { + $Show_Tag_Dates = 1; + } + elsif ($arg =~ /^-b$|^--branches$/) { + $Show_Branches = 1; + } + elsif ($arg =~ /^-F$|^--follow$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + push (@Follow_Branches, $narg); + } + elsif ($arg =~ /^--stdin$/) { + $Input_From_Stdin = 1; + } + elsif ($arg =~ /^--header$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + $ChangeLog_Header = &slurp_file ($narg); + if (! defined ($ChangeLog_Header)) { + $ChangeLog_Header = ""; + } + } + elsif ($arg =~ /^--xml-encoding$/) { + my $narg = shift (@ARGV) || die "$arg needs argument.\n"; + $XML_Encoding = $narg ; + } + elsif ($arg =~ /^--xml$/) { + $XML_Output = 1; + } + elsif ($arg =~ /^--hide-filenames$/) { + $Hide_Filenames = 1; + $After_Header = ""; + } + elsif ($arg =~ /^--ignore-tag$/ ) { + die "$arg needs argument.\n" + unless @ARGV; + push @ignore_tags, shift @ARGV; + } + else { + # Just add a filename as argument to the log command + $Log_Source_Command .= " '$arg'"; + } + } + + ## Check for contradictions... + + if ($Output_To_Stdout && $Distributed) { + print STDERR "cannot pass both --stdout and --distributed\n"; + $exit_with_admonishment = 1; + } + + if ($Output_To_Stdout && $output_file) { + print STDERR "cannot pass both --stdout and --file\n"; + $exit_with_admonishment = 1; + } + + if ($XML_Output && $Cumulative) { + print STDERR "cannot pass both --xml and --accum\n"; + $exit_with_admonishment = 1; + } + + # Or if any other error message has already been printed out, we + # just leave now: + if ($exit_with_admonishment) { + &usage (); + exit (1); + } + elsif ($Print_Usage) { + &usage (); + exit (0); + } + elsif ($Print_Version) { + &version (); + exit (0); + } + + ## Else no problems, so proceed. + + if ($output_file) { + $Log_File_Name = $output_file; + } +} + +sub slurp_file () +{ + my $filename = shift || die ("no filename passed to slurp_file()"); + my $retstr; + + open (SLURPEE, "<${filename}") or die ("unable to open $filename ($!)"); + my $saved_sep = $/; + undef $/; + $retstr = <SLURPEE>; + $/ = $saved_sep; + close (SLURPEE); + return $retstr; +} + +sub debug () +{ + if ($Debug) { + my $msg = shift; + print STDERR $msg; + } +} + +sub version () +{ + print "cvs2cl.pl version ${VERSION}; distributed under the GNU GPL.\n"; +} + +sub usage () +{ + &version (); + print <<'END_OF_INFO'; +Generate GNU-style ChangeLogs in CVS working copies. + +Notes about the output format(s): + + The default output of cvs2cl.pl is designed to be compact, formally + unambiguous, but still easy for humans to read. It is largely + self-explanatory, I hope; the one abbreviation that might not be + obvious is "utags". That stands for "universal tags" -- a + universal tag is one held by all the files in a given change entry. + + If you need output that's easy for a program to parse, use the + --xml option. Note that with XML output, just about all available + information is included with each change entry, whether you asked + for it or not, on the theory that your parser can ignore anything + it's not looking for. + +Notes about the options and arguments (the actual options are listed +last in this usage message): + + * The -I and -F options may appear multiple times. + + * To follow trunk revisions, use "-F trunk" ("-F TRUNK" also works). + This is okay because no would ever, ever be crazy enough to name a + branch "trunk", right? Right. + + * For the -U option, the UFILE should be formatted like + CVSROOT/users. That is, each line of UFILE looks like this + jrandom:jrandom@red-bean.com + or maybe even like this + jrandom:'Jesse Q. Random <jrandom@red-bean.com>' + Don't forget to quote the portion after the colon if necessary. + + * Many people want to filter by date. To do so, invoke cvs2cl.pl + like this: + cvs2cl.pl -l "-d'DATESPEC'" + where DATESPEC is any date specification valid for "cvs log -d". + (Note that CVS 1.10.7 and below requires there be no space between + -d and its argument). + +Options/Arguments: + + -h, -help, --help, or -? Show this usage and exit + --version Show version and exit + -r, --revisions Show revision numbers in output + -b, --branches Show branch names in revisions when possible + -t, --tags Show tags (symbolic names) in output + -T, --tagdates Show tags in output on their first occurance + --stdin Read from stdin, don't run cvs log + --stdout Output to stdout not to ChangeLog + -d, --distributed Put ChangeLogs in subdirs + -f FILE, --file FILE Write to FILE instead of "ChangeLog" + --fsf Use this if log data is in FSF ChangeLog style + -W SECS, --window SECS Window of time within which log entries unify + -U UFILE, --usermap UFILE Expand usernames to email addresses from UFILE + -R REGEXP, --regexp REGEXP Include only entries that match REGEXP + -I REGEXP, --ignore REGEXP Ignore files whose names match REGEXP + -C, --case-insensitive Any regexp matching is done case-insensitively + -F BRANCH, --follow BRANCH Show only revisions on or ancestral to BRANCH + -S, --separate-header Blank line between each header and log message + --no-wrap Don't auto-wrap log message (recommend -S also) + --gmt, --utc Show times in GMT/UTC instead of local time + --accum Add to an existing ChangeLog (incompat w/ --xml) + -w, --day-of-week Show day of week + --no-times Don't show times in output + --header FILE Get ChangeLog header from FILE ("-" means stdin) + --xml Output XML instead of ChangeLog format + --xml-encoding ENCODING Insert encoding clause in XML header + --hide-filenames Don't show filenames (ignored for XML output) + -P, --prune Don't show empty log messages + -g OPTS, --global-opts OPTS Invoke like this "cvs OPTS log ..." + -l OPTS, --log-opts OPTS Invoke like this "cvs ... log OPTS" + FILE1 [FILE2 ...] Show only log information for the named FILE(s) + +See http://www.red-bean.com/cvs2cl for maintenance and bug info. +END_OF_INFO +} + +__END__ + +=head1 NAME + +cvs2cl.pl - produces GNU-style ChangeLogs in CVS working copies, by + running "cvs log" and parsing the output. Shared log entries are + unified in an intuitive way. + +=head1 DESCRIPTION + +This script generates GNU-style ChangeLog files from CVS log +information. Basic usage: just run it inside a working copy and a +ChangeLog will appear. It requires repository access (i.e., 'cvs log' +must work). Run "cvs2cl.pl --help" to see more advanced options. + +See http://www.red-bean.com/cvs2cl for updates, and for instructions +on getting anonymous CVS access to this script. + +Maintainer: Karl Fogel <kfogel@red-bean.com> +Please report bugs to <bug-cvs2cl@red-bean.com>. + +=head1 README + +This script generates GNU-style ChangeLog files from CVS log +information. Basic usage: just run it inside a working copy and a +ChangeLog will appear. It requires repository access (i.e., 'cvs log' +must work). Run "cvs2cl.pl --help" to see more advanced options. + +See http://www.red-bean.com/cvs2cl for updates, and for instructions +on getting anonymous CVS access to this script. + +Maintainer: Karl Fogel <kfogel@red-bean.com> +Please report bugs to <bug-cvs2cl@red-bean.com>. + +=head1 PREREQUISITES + +This script requires C<Text::Wrap>, C<Time::Local>, and +C<File::Basename>. +It also seems to require C<Perl 5.004_04> or higher. + +=pod OSNAMES + +any + +=pod SCRIPT CATEGORIES + +Version_Control/CVS + +=cut + +-*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- + +Note about a bug-slash-opportunity: +----------------------------------- + +There's a bug in Text::Wrap, which affects cvs2cl. This script +reveals it: + + #!/usr/bin/perl -w + + use Text::Wrap; + + my $test_text = + "This script demonstrates a bug in Text::Wrap. The very long line + following this paragraph will be relocated relative to the surrounding + text: + + ==================================================================== + + See? When the bug happens, we'll get the line of equal signs below + this paragraph, even though it should be above."; + + # Print out the test text with no wrapping: + print "$test_text"; + print "\n"; + print "\n"; + + # Now print it out wrapped, and see the bug: + print wrap ("\t", " ", "$test_text"); + print "\n"; + print "\n"; + +If the line of equal signs were one shorter, then the bug doesn't +happen. Interesting. + +Anyway, rather than fix this in Text::Wrap, we might as well write a +new wrap() which has the following much-needed features: + +* initial indentation, like current Text::Wrap() +* subsequent line indentation, like current Text::Wrap() +* user chooses among: force-break long words, leave them alone, or die()? +* preserve existing indentation: chopped chunks from an indented line + are indented by same (like this line, not counting the asterisk!) +* optional list of things to preserve on line starts, default ">" + +Note that the last two are essentially the same concept, so unify in +implementation and give a good interface to controlling them. + +And how about: + +Optionally, when encounter a line pre-indented by same as previous +line, then strip the newline and refill, but indent by the same. +Yeah... + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..5d45ed1 --- /dev/null +++ b/debian/changelog @@ -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 index 0000000..575a51b --- /dev/null +++ b/debian/config @@ -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 index 0000000..4e94e29 --- /dev/null +++ b/debian/control @@ -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 index 0000000..5a2656f --- /dev/null +++ b/debian/copyright @@ -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 index 0000000..e254c5a --- /dev/null +++ b/debian/postinst @@ -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 index 0000000..8cd992c --- /dev/null +++ b/debian/prerm @@ -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 index 0000000..866f9f1 --- /dev/null +++ b/debian/rules @@ -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 index 0000000..1ec4847 --- /dev/null +++ b/debian/templates @@ -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 index 0000000..3ed49e9 --- /dev/null +++ b/debian/watch @@ -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 index 0000000..871edc0 --- /dev/null +++ b/design.txt @@ -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 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 index 0000000..50e77fb --- /dev/null +++ b/doc/GRUF3.0.stx @@ -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 index 0000000000000000000000000000000000000000..c6aa14de91bf714ed0430db225cd17aa4e14f76c GIT binary patch literal 31213 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 R+p%MY_f7AW={<Y*e*mzWs<Z$A literal 0 HcmV?d00001 diff --git a/doc/SCREENSHOTS b/doc/SCREENSHOTS new file mode 100644 index 0000000..778a4b8 --- /dev/null +++ b/doc/SCREENSHOTS @@ -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 index 0000000000000000000000000000000000000000..390ccb7b20824902e1ef68af289ed3b1c587ff18 GIT binary patch literal 22877 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 literal 0 HcmV?d00001 diff --git a/doc/icon.png b/doc/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c6aa14de91bf714ed0430db225cd17aa4e14f76c GIT binary patch literal 31213 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 R+p%MY_f7AW={<Y*e*mzWs<Z$A literal 0 HcmV?d00001 diff --git a/doc/interview.txt b/doc/interview.txt new file mode 100644 index 0000000..965da26 --- /dev/null +++ b/doc/interview.txt @@ -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 index 0000000000000000000000000000000000000000..cb254846263c79fdfcbb3b5045d22b39b720c9f6 GIT binary patch literal 62208 zcmbq(V{|4>&~|Lwww;Y_Zm_X!+qP})h8x?)#<p$qPI7;F&U@ZJ-`}^-shOUho~gQ~ z&vaGS)ltfdQiyPPa3CNch%(aRDj*=Bt=}RF1NGhFJeDHzeS&Zil~IHFF1|3Pk>A&_ zj?!8#ARzE){|V^)mjB0hBbKX#rmL8<nUSlNgFT6wm7N&~3pX<xJ2MMA9hB?(cMp>P zdZ;>>yLuQon}H}8nK(PxnVDJ{kw{uOn|U}m+qjS@Xm%#iQGkFzVL3Yg((uf^($>k& z;ljWov}EUkPUttuO-f~Lm*>02u@GG^waTU17Jcb+X&p502=u@F%||03wR;y=Pa4U8 zXA(*QE;S#(lzbWR1ppX4V*}X9OSRHXwd7YgrsgT+N*JOVDT?8$RV=cptnkXL#CJ@% zWacRpQ)pz^>3;GkCdXF7W+t5abd5NnNy!ZDbCg75OlVbK$Dei5{8DEn9UzZqG5eXk zS4HkCmn(TrRUKE%F~D7+8~VaBa$8(O$m<K)wZ@H3X-abaLQmSqJn#%EJLy`_ywlmN z*_If?T;&cr2^XT=vz;N-|1|?`vBUVws7-r6$g1t*k@ACpNkelHvOg1imjU@b!60;c zZOs^RT8$)EokkFjwLh3Tl<V!sSIt3G$eo91D$uH3^RILM^$zgv&A-`P!2rm^9;(6i zl@CId9-1)0CGt)keE)^bbKtZv2X+Z8`a}gHIHK(CpTT5yTZR4=yr`ynJNneP-`NQ_ z46y@m<ta;TC#bnScV@iz$)~v!8tMh}rJxRK*kVQS+pU?}jlQ$7jU{@6x@R8zTt@`9 zfO5=JR9UD%!9hL7ezAUG3a4b2+!OB+Cjx^Gn$|<fKj=UC@O1;HuJS9Il=B4g?h|g2 zT@j#We9@nfYknTIDOFUde$jq~frMZRjJlVD&N2a=fY3PkP0<^pw18ese(=URZVI9? z481r!$AK?Ii=g)z7m~Xb6Yd!sn*rD_#L|L(6g}qr(+}GK7~j*2g`k5EhmcYm^v)61 zuRD&mB7<YD_%$IKAo&JoAo-oa+fF0Aa3?gT_eL@h26z|(eUHmB|3<$Z{fhov3k8a3 zU@^4SU-4SNU5q#msW==U5zKA%7nb_cfOhX;Lasgwu*xgYZ}oMT+Y4A5l_^j{axCAh z_4_*xs<GD_0nZXsu^aQH@moHJGj}%t`Q5HC87PIuU=*jV37@wwgq@(h+l%vQSW~tS zw`2NcFlI3cbmY6p*_1M{JZS_Kpi@o@Q0wYh>mBd{nU1JsnC6@UuNnpZQjP*52J(0x zY3v7(e+{2*&Tj;2KO@&Vm<bgit$MAmqOa@+e2^C$F6<TEZ0jAx6ie>g+}8>V7FwvR zuv&1zLp;4YfTq1^B(XTI&-TiGR2{wM%$3jB;`#Guo^>kZDZ71D01pgq?4^Mq)ZoL# z)kWAf<3es3Eo9+>e`*>A%<pYR0zS*_f#Okh91f9{cg$Nr0Td<nIc}xtmHVeqAr~yQ zrGQiipkQx*7R{_aO&R0d$h{9%phgGK0IKiRVfGiu74+*SVNM)Ah@qd`#|kU-s8QbH zBT%~xM49VL3IP0UpN86f&3FQip?}Jy8eE1a!&{rfECP3)=@Xub?aea_Pbk@US2i7} zc7i+Z3Ji@8%g&JVr))v~`3C^P?+Pw!-#?Uil{lCOtnZZWc4)XvI2a|8falLbU$+ak z!CxATy^kD)`JWrBOey{!Y&lY2%^w}lwI1l-Z2`4jZ|W;wGg7f<TcG6@z<XK};5oFi zYY(qGi1nI~PuN5)k%kXSDhD#qU=L{ZG^}>J!uI8MS)k>INak_0(h%@FAp2;g9@uY| zuuRNg@OH6^@hrTU3Eb*(&UCVPxbVGN(gj8M`3%HY5_~p~_HVlDKlM;1e5W2r1_r!> z*0>`y&fJ<j-I`=}cUd;{$k^4D(@M?^!17PfuXvS|-FK$tymym@ik!VON2n2#O1`P* zZBzB?F-Lo+dF~gHbZ@=Hw&XK9M|_T2_y%b6Q>`R8<RyP`HLBiJZ1Afq&|;dSUsr8( zMI4WxE%PnQXaP%D1r9LtXa$b!+Y=ko0$wI2GRh5H$56bGz>ER6deF}||5_dVdWe>z zEw%~TmpFKccZxn(bY}y~7m3fW#cpof#Q^BW(+^)OLM_ZBe^RDsYErhuh~j(6Xgqg& zpDj}OKGAs6pbruTDQ+{vo#T&Dp<Lb)18Qm_p88#}CPUUZ5bVJ_#@Z(*Bm}lYyoFb` zEU@=J0pM?hysoFSMPd<1|9=JlmDv1ltaF*^p!D0DNZZ+nO1^LZ`}(d!e9Ql-(3Y|n zfq>xV+u5k8etR7~9hVKsh7t3F)#Mu)I-knvm`cUG^)gdXlrDkjVrjZR6yc?&Jj_h` zAUXJ2_p%LW#8?5hUoi_??BEDc*kCA>|JGP1hRj$(&T4&n&d#!mo((u_PX2yP+uAd} z_YzDVKbIcH+oYWDmv$aZO%3DoYr6a0dDosz<*E41t>Jq=B-=5+cY-_UhaK!%LMd^r zD@Tt48NIizpStEkYV)r4jy63_W8<RkmMw9}59Tv*Q--(u1vdl0US@W``hIZpAN*To z>qzuAZ;SDNaEu+cCssb&2wCeoT*it!-nPAFzykmp+u@<x1hhGzI!S_{TcH^-ezTq` z|B=if)Sojt3B<17D8Beg%(vrQEmGn}UY!y_&d+1O8-&^-ylxl_q`YptUo~>@LEy&b z3BBt3gg5a<H<}tK&gRBgnxr^?h|~u-KEqxsAg*L?>vk@8FI+!Fx;xM`Xld>)oxjpz zzjDI3Ll*1yEwwwr0h!xQ6WUJ24g|oAKho){^<q+%iImvJbXvsjvuWXKe#mbpPeHW& zv#%Tr*{)5jXuv^ut@nUwrc2m`5*mm8H;6)GQIY70fI4B4Xt;!m?vM(a>_o{<K_cd! zL(P9#KMF!hf|8WXX%@LF!^(apd_LC3C0J%0?42?>#I$nHB7!7Vp@Bn}E@0G_`UjUG zY>$LQ)U?hx6d~^sIScb?&kyVH%%e?RM336UGm#je*TGEw7E0cZ=<Wi_KOcCb)W>;) zNdOKdJM4ru@N*UGH*;A1acQ4s!=SL@0}DdX7zrSaRAd>lnG9i1l1Q2e5I4p71@ZuZ zIGd2xV6FR!rCZG_6@E&`&X$tBTTHMTxRM5&V?mHZ$P%IYIvMLh@;UYjGxU&k&`6p! z3H?Keds?-=-ZQ~8jm<+Q{cOlXwj$Q_z9k~gdyLirmp09sOHA^2A%k=&cm8=Q=+7J$ zf=x^w19R<BTkJ>ihDZP+_6sLRM3v^xs_1{1z4TKRBh{o<S4Vg=M;?gYh|}qDqUOx0 zpYSLH2nBGYyQ#Qf%bG!f9)sj5Ml3QW5FD#Vzm&xB4ssLQEiR(Pe%+vp{v<mUJI{>V zWoQP~d|GM=TzcQf?-`;wu}J<SO^ka@mPM~U2|lx*w9Z-McCL{A1U?Cg$b+}YPT&j# zwKn&eOb9d)bZy|dgkv`irq*eU9F6dv(7Rq|dKxU(z_@*s7{zr~hd+3nbzmevXr(-G z6|AgaPb}}3Bgx`!CXFx}%3ahX7rh}eYG~9-5^6#{4g5DhdnSVH3}w+*LXg+%<jQa$ zC7l;^a0GT?8<gS>#k7iWZI2Fb1h0D>t7UZDeFpPmX%2ZIHY?A(7{zu(#4#gy#2bSL zf|DKYJsG}(e8jc<=WO#wDFYYYK?_a`W%v)tsw*js_61!TcC%b1eP{_Vy#CC%ni7*{ z)1jfn(yNz#b^`1j#cGFSE<Dzl1c|mA5vEKIjGw#b(SlJ0V;};@AkC2kENDL<NCdH9 zk`IxR%?PT^toW?HXk(H%idl<Y=Onu7<owrtWOvF|_Z)BpWS*9&>&Z3vhLy{A#y0YE zeKp6z`*YQB39F8eg->v$%EnP;MyQI2rw^)?EgikO4kULu#GA-@lgGowBR0k>9GL0Q zf?fVbO#$%O4^v)Ao9dL%CTKWFdgJPhb9ebjrX!&dqy&TD>~~#YymS|3{%9J_QYFGQ z@>bNKF;+7>smxI}>tQS6&5`Qxg5mp@`G}A%Aj^sVfrC>U&J^hi7JY)6WMPvSTO8l5 zjF75B6v6JYIvo8iRdclDTp-s!?84?4e1gG(#9Dn)r)-eJnRuC?^KnPmxquQv;55G{ zl4Rn432z@2!$|F@z+WdZP=4n`KEfHbOD1;I%(LS}8-q(|9c*>`Z<cm2+u;XF1S``9 z$Y!%@_DE!MHQg6Yx@W_W#~%}k1T5LH#$hj3)SSxytRhSk-fjF?rRFZ6T`)M_@x^iJ zkjgUr9gl)1p`pP%f^qLHp(bqwf90>jS*jYb41r9LGliz}127?z^I*ip#4DD_N?obx z|K#*s>CFX#mc=nQ{@^7OXQpzmPV!lZBHX_ICp$*E*=y1pfuik{elp|hwKoR8MQmyv zo0FV06?v?b5t}dR1q}lh%b;3L35kGOiiV-I?LqNw%=#lTwpEM#A}XpM$*rWmRL=uP ze%U#*C#K9+P%0g<&01uaEw0LQ_a8njyTE2_t`iJ5HOKYiNuQ?Dszo@*sY~fACz(t* zmV`XQ44_LQ51*Lq>dGK+EmB#|!QrSl<bl9t4uNpIE8RO|qrZH!(z&(hUmY3_YO@Cf z>u~H`%1>{GFu&<Oc#o(Z3#T7iW)XX8u*~MW!e-hN%25*kVkM#n^0O>*_%?DA5{6;t zRKsQ_tJtH)BO`x~<2PhIMoKXDZde_|oZhB$y6*=nucu|PWrTtxA{JR`CdVnqs1S^q znhvR%!1K@`Wz?hZe=qR7eE+Cvj-ABDq>Xy$5@e}l$K5V32_f*?%~js6o<$PuiyPt| zcr&+FP;GW<4~YK$fu&+jb6_<#X|ED$iqT%5!ZsLqM>w+NIgkF=h6JX3CQothCMikX zM8k^!6wl&N&Y#Rc`q2&fhazQ(mZ7xDRKqUigl8!R(IqW}THM9Vk10bIhJ-#L$Do7? z@`73K0l93Wei{_X+*V7J*edA-aEf|VF)WfXI2kJ*ij=yw*?-~X4u_$a;YvG8uZM)V z$PV^J!)imy$<WI1d?2v!;mL`9`yRG8v~|Hr+}v#VHN~2==SJFVC01X~O)8WZ3{FxA z@<TFGxdbuj`|j379~8)%0~)-c01Z>`2`omPdX6-^sjRwIXZJD{tKcUJjV@ty-*dpq z$<XwTQSL;E;u*|P4HJD+EYIKJC1Y&lJv1FyP(E?d>Y1D96p=-MR`@tOY>JR2Xg?Ms zU@{JFc-gtS;~~i@oiCDy%K6o_P@rOjp{{UDU<-wMH177bWEAibbPnPAns}{=aAj-^ zI#GvJk5zE&GNQd-7Ok=uU%eG+_NCH|72o1n|7rBmwi-K459Ne)tvf!*U(6KRtZmLh z#gL^JZVc}@_}kEn{1>FMn!<84q|<q*Fu^c-b`F}g(d?@e^?2vMs5$dpJy2e|>|@~P zvX&rLI?IHTao$f*+10oVE&sW5uG$kuuZwUg7z9gHaLpj0g59J;=%%_oa3kl`%mGxZ zdb(zupRJne+MVLUfBjR4&R(?e-VS5Z`LPT+_wWN987dYf6nSLjX@6l$+W^$PU@@~^ zFEIXW)^;WT^45LV+Y6OCtXq?h-3fdgt_=SLz$p}5H43}w_v(zmAJewg$&>+NsU%Y- zzs9r5`-E5>PfgUrVUZ@^8p!3d-@WFJ$C}FvCVf;6yc2Yn-;L1@WBi(jn<%NX&^7WC zHV2#E_XOk>w^;Nz@8|RUyOn2Ek8h%TD0g0P@WBZb*HFE#<EQ;WJ0GtyA6+pyM>kQ@ zP?$kD$2My?S3gRAz5n%d;M3Te&W027r)t7xR8&*v<N-L66Od-lpOsU+IXk+%LLyS$ zk3W<*7$61v5>rv}XDL-mL8(34{cSoYLEOyQ;VG4#>N=|Aw-Sb`u^v5Z0vfS1C6}fB z&3r-L-N#>XY=1u08!XjXaeA7BU@qTLBArnw?bX8gfnW?b46ITN)nzsMD(RW0Y%V}& zQa*iKK&oSJX14C%KV;<9`Y3Qudd~_wCn*d60|0*MG|J&e4J%i6X~+vkF{X>hfb!yF z3FX<awMBoul?+Ntw)<%%Mu)SghS<7Ev!yCcL!_R)V<*8Z(y>i4dEKaNF6yG3TO$!0 z*aLMZl?oxg(4JZugUaZUla+0lDm_`fC~zQ3UFc$QSgHDIN&Z7Tr$0mE6SJuYe=bEy z7W5}$33WfARr5blrL})r*upbEgfrRx5Z-Du!Jtw5nnERT@+>ec4HS=4S!BEWsVT7W zED&fccBM5{FQrzeYnR9~9_woa=t147oX_?{#Bw){g_t)RPZnb&vz8C7Hc6GRfb@K9 zFNPdC8tY9}(A{zwG%=MM&?vuH!8Sm6qRsk4o*T-!CqL?E>u^LJ)NXi>cYtgsjv+#` z8d3K}T}f0a4yzzk9>#FwFH;>k8c!+xmA74~9D>{TUbr*GgY*?p*YvRXC}8<T12Ibl zExld@Z6(KRbX-XJWPBP_@u@Z&qfS@pK=x#uSzk%<Yl2NT%~N&IR-e;^+PAi<*Ivv} zK(uK?!I2~7VvvVl6@UEKbVs2VQScuHKIPjr${v{pM->P2>{p@sbteWF9cC@;u7ppg z2Pr|n1R>m4BOX`@%9f)cX>uew+XNK?D@^*+t3#^Xv7<ZiCg$qv3wL=Bt0?Zj?j_9u z{x&-1NG)2~;v`RDts_sBoQh;_6K?!vxggeNhl=u)%BYFnO@?xRe^SM?tt=UE-|g2# zyJ)`zm=WA7wBDq@D7tSEq+Q4m+E?4#D^JrEcX$3he)4UlRQS~au|)J2V%U8y@W$qy z5R!G<k;ls)<zgGVv%CMPtGzMCpcZCCZ?HrlNV>*n_oUvA?sYKu$N2mvPk33+@z4bm zmdZ9F&0FdSTMa9R;6aiQ5PJ|#;7!-txnNmeZ%!O(1H>gMGv-PiW*jRMZ02e9m6!!% zhGmUfZGe3}?uqXvL2teO_wi2P$Dc=9T>bLcvd0oe4LRA{(gf`R6S}=rsgM10=A&QE zNR~Desilz<v`f1E_8CCQOG;mDj=rcKoisg0vy|<G8^O?qukq^86F-n<3925o>U~y{ zrzk3P$GPReUz>U|>8SpLa`lo~LSCRd3Oxs2;&N)q&i1c71`FDbP1mo~#1Xc>1^!ot zGnV&dz?DpVb=ki-SibUuT>hahL+~@V+o}HeHBc{=i35pr3n}WaEhLb&ShAmBIqEtQ z(ayJ}bQ2ny<ikXRc6+hF<-XswS+#|~%qVA=Qs3j(&wN_svLE3-Ek*P4=aE5r$aAoJ zfdP_lOrHe5mE)-&4-_+4Vimph{lsodwC?bL^j2cSbYn*=%HBPt(qA5yki9`yE#;6M z7(TtbjCUya1;OU4JpkmwA{Wtnu<LOw7l;|+Iz;mi1!iGr_IHsX)YW&aorOCGy0zbV zjpD03ZSSNf=YnZZ*VCRI)n3{UO~+PK3)y8ZZ;QRjuDzn|9-~W?Q%l)ZoEkPY%er|{ zRoo@IcB>vE%kkzmQ_I<ZIdyDWriYey@RBS!1M<@+RQ$?3Mz&kb9j3OkyE%Pqo~MVl z$(ZINHebzC&uiK`irckanvbpjdnLP5bEeJE&d-o%XAAzn4dMTznNta9@!*P3aQ5W$ zpVl3dnidWXU`1Q@=TcsdW3zs5+5cMkl2i8dF7Us`^#FDMuH0C)EZhEF({6mq=v(&w z%>Q3A0-`SeyZMda@3;P^&lBrzf2IG$dIPS+y$5S5QlU=IKUJ?ot5nt)`41FxcvOGe zThh?MZ7^sC!&+5G=3)(D$!qOh<4x<?rbDDaMb#VPZOyZDkjtWs1gE(Z?XV5L(+A8e zX{jF4;m+66!`|M<XZniqRpg<ysBGCHpakaGpyiXN{pyBU@+qdJ$71u{6j}U9j@St> zn8|!!{w&}8ozm|7ZK>(=r}ZE!9V0bGK-F#gaN<c$ou@~)_mQ%+@!g+9a--OnSYv3( zk0z!||3CC@7)DpRb~ZI%s!p8u16ZUJI8&-^?_TAE4oRh#4m!EA<edsu+#ZQ7vLn;e z<ub93d{;~3Yx;KYjY*t!t~fU#%k_5ax2&Jf(J!~SL%Px`C0N&UmgqWHWF8ZhG|$bO z=Xrc}qc`ONKzTR8lCH}ctH`a2#}j|@FEOqhU$lmC0Gb8ONmB-!g<Zlco>n&;F?2q& zJu~k#3btO_0YXyu94fvqm|I@3!CjQ;W-<r!7t12o-4ai!+vl<uZhiL=Rv?~hVpauq z>lheAc?J5}p3M-srI{tICAsQrg1?boec(p_txsEiQYlW+SBN&%;4ZkA3j2Y<V+0>F zW>tGAHOc|(QYh<{PMg0)EPD67WByB*1LDa$eW7yv_s{8hO?Qx!FFRuqFacFc&PLO! z-IMmyn|p3lCPi>8P4E4Xn^LCW3rpR*{iE)Y8Lc$HBC<C(?a+sswD35#YNrtgBfb01 zivdk*`}s$x<fFj(7%NdyegBuff$x<XK?q*>!|>9L6$0LwgAZ2B@IAL(c%t$S>d0Pi z5Ip{EsuaR7e^6_m1oqJHv45H~^aZ_jtzSaDP0&MeI<I*ttpS0p`mg!CFHx}p<^E}R z=>BkZUa+3HZ=Ukq92dhEfoYiMGpvNHl3X59Qx_b6GQs{dk9l6XuYNepI`{?KT*A-5 z8eF25NVgHhI06Vp3t!X!N%09h2rO9g<<J~ij5jmctfhY4ZeWc9U4MTag$$L;ZQ`7e zo&v}v<dt;_R7XniRn%bS@~B##8HnK4e~amFhDx%LM>8*C5@TNZVnmB54@H^JZt~Ti zVJ990Z-M$LocV~hxLFTP6)`UWp*BU9yQ$y}5UJY)sZ*~BsabraqDGU4jN+*sBD8mS zUP_p;uZV=#uMulcsJ93|7J?mCjZilmC(PWjl@{;d-caZnA7p0HYnwJliw$(FDlYb0 zGbAHS-=JgXXG4JE?!h-A@%nMyN`5}AH+kIwN<88WWQbvP-90B*bzLW-N?h9J>AB1_ z#u>d4eqcO`XKPVA9$|ke1$ldz@INHCOVlktY*lYeo#5Y-$Y@nd(AfR4er7+#77)uM z@4tt}_yhsN33_QZj|YDuY$O<`-soYNs74u!Z?wj1o<@fKcNpFhXCMN}*wZ&#u{gx` zMPPbnbh!KRz?J%eT5<;YiVGM)^DXh<B=gVjn=49-V-w1-GAG^OzrVVDXpMek$Mb3Q zngj`YvNTUIHEaB7eB2>Dxl{7yo#MKWdQX&_@mq^Hr75VS&TaGqkSa;@I?CuhNJ%iN zHm?@-wv#T|^K4O`DHwb~##=`+MegtX;f2jA*sDY|vC^~vx9ZA>;}B`%>0NM6pjVZK zZ3iGK`9#w+7k%O{andoo(Ft#r6zF1EKdjeRfhFG;E6Q5(y!vy~oN=A^OFxneM5Q;y z(a}WebSEMocP`Rrvff+HA$Q<Y>B~COL8Q1KG9g7Mjc@K<Owhnz<icb6BaeTt^Nmx0 z^wWqqgkDWEVQ@KC!YB7!o3k6$F2F|oOXl@D?gxs7?doIiBsr#AbesnHUxn%?%?zZT zn4>;geiwzmuo`|{oh7e7yY%zL{?1r7lgO>zukc|W4LBknZ5|Mv_qJ8rHbb5t-EdH2 zoYFITw~GqM{Pve(9Tz5cu#$I^@#*{3-)eOvWm0UgXGxg~j7a^^u(DASYk6gS`z(Nc zA`JN}2S7MMB4*vwF4VIxx~~+ttXT6t*%xq&8!=<jB0g}?h2%oI^9UPppuD%Z%h5Zi zSN#0Y)3nIm^ytU-a%~b}JPV#$s8Qfn?eO6Cvr*dy{7E?pt>3L>6fB8x&@J$H^xtE8 zY?OcQoL%z3Fw1gsumDk+e#O0x#{J1pH@jRyg5*5$UJ?AOPRjjsviJthtIq7a_Qo7a zD(%^xpQNRLuIo$d!+05ViO8pMsYZUzr#u+#k*Kn8`eH{c%_Htp@21EZBUR&5j^SKC zfuXaUfpWA#Pt9PDhI1u8`IK4Lcbu<+>(lk}1}Qe6go*90;dLnusky*qYCJooS5vXZ z?D((?aQNk$TYcP)oLl{z79cCBe~BEJjNN^;_taItkT)eTQyLJ1)oBdG`kOzqnLsl= z>W>SrWqb};)E_Dnl$Af@MjmzXoTy_R9JG%?3i({y46oYzPJwHGQVGo8G&Ug-VGX8? zJmq%Zfi}@{jqri<=<CfGyeOJKWVfH*9ys38V?YcbycE6<c2nu9OFfTO6rB#HEJw>~ zU*<o4=hHt)mmSOGp03<vBDcBrHUtb`-qo}n#+X6DZR)o!zSGo58+@0urGp@{PJ822 z=9V63h(a&^SyL091a%W)?x&=>SIb01{zSX%<Ug((CP=Yf9tuSg-F?(H3u834`ca9p zFJ}o+&Y>O3xwiU2exD+|u!>pypa`~BDG>(;<i$5_8amAvpXXkx>P38hvQ9)rq#Sb* zAwB0^@c2$0r$G|(yVtGut06hs?mZY196=Hmt|7N}rnUrtSOK6F%26G)UJ!RTxBS7) zKPuzE2%fSWeM`H=E}4sau+{~rZf;PXrv#GS%VK`@#86&!4IFM=3ucb_1HAp6SBule zJ<ECEy^0#leqot1FSq?e7@=Fq99}dHp^d9@o9XAf1e+|Q(rZ(Z9Zyz~HRJgWMGN3l zSwKkUr%h2!Hc&%kI#02>)%3GA%H$#|!bPi7tH%VLc+!0P$EndZUg`DQTcW4Vre9)e z;#Pa2hQ8JfU7wHbZ{cl+4kA-!`pHVsuWV2MXg*H8H)C5_Y;~UDBH?Far!MGc_eliE zO9f8On4I-L#@4laK9iGE#>eJ=a&!*WW88ba1RRL-N!YFixe>CScOf{zN4YP@Uf0>~ zz`e3}d0KNGJKP^I$=yGYH3&UrI=y0Rc?nQ}L`W?a3ZFv3_;GepV+1?jR8RXi`}57z z=qXOX4X~hWmK5w!mtq=7<p?4usAWh0ZEfK|xg|4Zn)vFBQb&1V<74NWC}(rZ)Yvo@ zk2{3IZARA>w`S3=v1GOmndALKw*Uox-uMJ9v!{B@PN8$Ae61ZJqxHBgsKFM<Uk*Kh z@Wa(Hgpc1Slt;YuapQP?)Dx?r3qh1BO{@qmVv03jC2O~Kv*Ioex5Q7gBgopXxy?uo z9$1N0F)SfuJ4HuhfcBZ_^h#+Xx#I=;q^sL7E7V;80z!xX4MOdG`rj9*H=Xo1DA4~C zhxPsNH+AJl-$y~8Z2vFSO7)L?7FHHo)DT$`ZN0RqECX7UxN(v+1y)onzFGl0A75jm z#&UG@!|Ap6hn_Hh4>M6}^{9JYCCI9Cg3iSgkz>Ei9^|UWUMzCp4A_>T7CxkAAh=~< z&LCo5CI78f>zI2_xSFquJ*I*G2!7y-BadNlC*GLCj?dn2@Z1YfLdBx#I$y08zl;fY zVJH+G=9@ykD&;pWVFOH!lk30>@aA-9o3vZx;z=n80Y?757r=EJzA=wv#nwA1X%gFx za}P<Q0TwJuaC-rPBe8l1vF}X>YobQ;<;KNMv)9#Da6O27sW|D6um~LS@UnXWFIMNo zUpZ~AqlPcSYhhKLO^3)mu<kFe;pc<F+#)^?-VRTH6(EuGc{G`5wiy2$+`Mh36B;Na zn7q6<y=`0+Kp&mbApi7K)W^iv@B8gZW$4(Z=Ay5r=98!lD%8%??GqMacw6MtvlU{} z3hv_!*f-bbu3vDyeFa53Pit*!Z}6CaVnFh+J|GfbS9;mpF$j?QeV72|W0&E+BuLf! z&ZspfNTdtmwYV1UwdxACX97Rf@3vpdOi_`KonLx17Z`DDa0a&(8II@fzV99u8Zi}i zsoqmAOOPgfRo%~#hp!EIe>alI2<O6;_WZ>wIW)}m_)}1BLDlPp>6r?9!{;t!g5OWX z^pF=u4pZ?XO^EFl9XkYwV9%agxBGm_&48vvpdg`0j)E^OX(;puixDo*x)`IU`)iW4 z_{AC#Bg6+A(wY7=caV@$WC5qA)2Z4LAtB~~LbakcC|-CIu)RSUOX<sFI1)DN$5p)Y z2F)ateQ%Aao^^qN0$T{=mJ<~NYdA3QVLaZrSgHB(xO`@)iXMzvnPrx%a}RH8xh;8u z1NqK)1WCuxArv_9I(i*p?+X7As-YHu(`VK-2LckO+w(e@{c$;=pj7K}@oGMx@4Gvk z=O-~__xgOF0hd+CClw|M{?fnr5N|)HF9&LzSJ12TXezRmJOA4&o~X~jqO6^FUci!5 zfSk@3oE^%0ph&WRl$;`fXtG2nQD5r=_Dn*F_HL2*Yb`L37_-O!t3BtXZLdpaEGg$Q z_Q%=RE^Ia-Q5r(qQ1?MC6_VJX9~%)z3A{XfA()^sx+e70x~61+O|#{u<d%j^D`<TQ zQ{O852csopdWhGhdT$Mg;Wkur(TODDwH?2or3&s5q`TU@c|sMEU%s#D<KZwtv6`?_ zkFxHX%sVwn=x0AmFwV^)r8;A%@yGi0;_EP5-EvA&wU=kX%sb@L-QlTe_VSK???}n= zf=Q6bPzr$mcYII&PjAtB!n{6s<_%`>V^O^oBM{>QZ~IP{(k*dJaZdjoWM>-R<#@&N zCh$1xJ>eRb5JNwX^8TA6)E8Pqk*m=%sB6#9=Xah_2}YfSzxDvga2(#&U3sg&V*Ugm z9TAj;Nj@!2XEQiZtuM3t{H(HFrb$nMZS7_O<?eQyeAQ|RhzEjI9zDwAsI;2nQzh64 zUvlBvKQlB*>HT`T{l)M!g&9fa1TS#;NxO2QbZ{8aX#8KJa{^PTmd)YcAwB=G_Q^ws z2i6I1VwZ|Chc|;-0?|x<b1g2povR%y+7T}vCJ6O|=!fB(aDpqgP6YUT?!p=qC%(s_ zF?2HFb20nHoHmBW*M{o;k<=x1TJ()uW)AZ~4<8}1GVRPMLJa`ZfeO~cUjhf%?3CTA z_j^X=$e-obc4XxldRec8#ad-FbQzok%+7!t`It*T&NBC|YXk@kMS^^IlTn@s$ROxa zM54!X{MFC7NO=I`DJ8L-I6PR&b<16k2DrNx<<+B+ve@I$m_qP*_f#ipLG|;M`Xxue z&%c0zu)U5(-@P-shSqxR4k%0<RzMwKFLu>597FHBEobNWl#hQE6NV$i6L6dSWRC^2 zHzdIXoVN7IFe3zyb9e6>bDjy@BKw`b+qQC|nX4Ff2r>6|e?_UrWs!QZIyR7GMO%(x zH{gi~oyg9x)a$KIN6+8{CqU7SwlQl>KM+PEQXP!7k(XngvPtQUI{6T@1gmLIus54& zl@1+95@zS(;!3T16N>oxd<K=?ZQmBA7^L(tkAa6gg~wo+BtEcW^HaA2K{D!OT3<~< zJW^soha+r6uq8K3qaE<Tvl<vu`wMZdAUpc-3C9(w4vJh(cA*B3-O&!^OYrWlo?;vm z?XPA%YVlV+9PruHK1xqpZwIMcg=?(NSo}y5-3B~jZ2;k<erUNtfZNOyyPoLWJ+G3l zzyYV{YqN1jl*Su$2;KY!7y-rWGE$aWHk$jZr-Ef&|4P$6ton~mGZ_Z&vQYd$o!vP) zvhmjq@$L>0#ckx)Onat*i*5GE%$P2p`o)Em*;ifPdxPP1$6)svF8jCbJ=wr>$n}0> z1*k6!3Som6;C=jD+bX`riVKC++*TuQIhy51Q)>m66W77A*wKC<31haYWWv6u(9MeF z!q8RDKlK`$8s(ae*QF}(C;Tj>6NrrV+Q5D!Wnz*Ll0vXj8cARWu%s0{-a=y|{_8y0 z8wT6PuQ<Rp9jgdW%HI)x(Oq>zHR!o(Er?^ZY*QsDw>YT`H|U_#s7`NhA2<{rykW8= zAOg&UB`=kc{o$Ftc<kxwt9a-3;nRb(<tpyHvVs03GWSZW9N;j-sHR*sor4`d^J_^j z#2EmVuLNCqn1ZJAM?hG=CXT#%$&rcp(lI3k+SE&#FY9d%d~*O2Jk&oygL)pa`R|Ut z&}8mq5BG#SsK0sZ?A)Cq*5g&TZp@=8&!TgFgd}w&bQBPuJT&*8|HQLCBRj=7z6!`D z#|N(C*Pds9$>?UP$rzOvAF*b<B*o*qB+7(*;{8o+fd8tV4VNiY_bVKdiI9I6ytwaM zun_`ipH1fM3j0HEo5-M$F$ZH7Qx(D&YvFWvu-%DUNGJ412p`7U`?EC>h*yC>C#ct< zQDviYyuv4OUw|M_tfYr0Jw-6SuwWfSa1Z$r)+E?b2^NgzTJsIg^3*1)`^!&GN)*>j zROG#+P}!G~oC6%-5aB2mIp<_Ld%voH3L+kg*@4bv|HjLk(yFz}ZY`FDInlksPL#Hz z1P|p$=4nrp>%x&q-G0JvWR*hJ{0F9*%48DgXvZH~9)U%#mgC3od>+F3x30Zzm=;Xu z6_A=Rt@F~gcQ<O6Mc<$Co{;UKK6o4IGM%)MzLGHS$#r8d5+?3borv6Eme*&4E{!zW z-mB39XVyNM7bx6;2(E3><M{4zeOm2ET>iFJrab)g{B>Mdyd|)Xv>4&r{iS{Z;%a-k z!R>#Tl`4#%$O22BQ3V~^V2Kh#xj(2&9TYLRTX`@z8I6iex+(8wm<ZwO$ej7My3;<` z$`Ts{N}B%g&Wma*%?KN7AyV@M8@Zz&fQ7C7!kYJtBB#_{9AbcXQow(eOgE4=1rdiK z<>$+1k4tTa<rj8KH-1J=!x3kR6)if$qh=X!5?_@2TBA5N+;WV&A1)k5X?|TYROp!Q zP+M?$x`m#Loy}7Uxr7d6xN$#lHU+Or2V1N@VQ?}L&wRSUXXM7;5kTmi<UaTXLHyB_ z6-9z3_)+F=XwEt;N+0}yk0fM>Q4`!hb%45>mhri9A+*#9p#R?FD3|z)%mMlS9fPYc z0aUHu@qK7GTYbqU{jt&ym#qD;FLDdSm>_iYE^9mv=@^pxtCEm3NScnFMy=S**IKI7 zYhiA<a%25PqWsY;i}lAHqiDyo){>TKVTifTnEYG3U}}v(SRQo<2MSriv0t9<<AUet zVS6zfI+Dc6GpN!@zX_nq-SbzG?+L!bbYH=GQM_om3*}fD(PHlF_auZNq`rkrVX@@H z!p#{Uo3+$2qf=KV5MgQfDC35Q#$A>7$lnqY(rF)VA6R2ahf&YcOU!|@J9^h=(0mXx ztemej2c33yv(B0(L4XO4I4Jyn^>h#w5S`KZ1wBb_$Rnw@%j+YCFnEL3y5UOw93oKh zcZ0*7X8{LeX1%}>v8UQ~6Nl)BE?QcDBe$<NG|P22jO$gwB5co|(oLW%XJIx1XS#y# z7O*6a84=NC4fS;BY*`@h$IcFJQEdPbf*Lz_sN&ph`I?^2Cpff}jbn`w7BD?eel!s< z2~LZ7P_Uh3-ckRK&X&P&Cgd&kjwz=xT9xs6@m34x156Aw3y_;md1XEwZ}h`dioDhQ zssfoMsrm(e^1c059t_(hd3Q0!=Xkc#30zuw`k&F+(;1=nfT($t2URais^DAgb`fiv z>nY8g`QgeFaY?}Hfo^Lf<v6yVim;pV#)JqBIx5sOm~aAg34Rs}r~+{dp|_xY1Ojt< z0`H+u26wY9mz$pdbG+RNHGBNA8}rylixT0P(v4;><D-!5<M-g$K$VuVJD$9cD=7Y& zv3HAaQpFd91Uc__qHskKj8jxSumJm5OJwNAB8v4IGq3T^8nHn&=K{Y?xl8d$PK4xW zCS7la(+ZkwBud}W6uQ3s4vZ*=OQc>teaEb?{RaFyo+cNhYrbefnm$@qsnI0GBBBYr zxSIQ`Xz@`f@|ko{p#PS6T4e9UVf6LN@<XjCV2dF=(*L|4hrY8QQr1MvK;-WIj*67) z)QW#E=gtBA)8k4xr<MdbI^Q)w3yq|)ckkNtSeTuHh#F<_NZ0575DGy$2^~eA(AzlH zmh}hTigJH$7m}5N2`1g%2jL8?Nmcp{e+MkUn14t!x;1wvRBPiXRWs(&EM^&dUl<@5 zMJw=K$2KDkfcgG%Jw=vVsjj#s3y$kcv{!IWy62EgrQppQL{G_rR`Ff<V8mt=f90S~ z_dSCzx38Rjp3`kgV3)6ud--UOku7o^-QE`y$m-{~<LNSkDx?sA)4FR0_bO@XCSJJ{ z)%DbWqqv6yYzumjLQ-MgZgG^oLzgEMJugTX#r#*8X*uXx?+!3HTPCe%(oN2$dWRmy z<!WBfvj6s32oi)B{mH6fs3S6qF>{G1T;Fk43+2QX+qxI^w$#RK4hK(U3a(<ggfEO@ zfPHDzKl+6CQ9e6<S{m_2rY9l5@sm7B<BEZD2w-OXIsjp8Wa@LE1Zwy`W+u_<#}iI! zCwh$7+Q;yob)7s67>V>+0f;mQmKUiP*<7Dq8iSX1>%k~i47QOVL{7v;v?hyV8LR*& zf~*NS^Q@66-75O^SK1B{46l_yNm@fmEO<;L5QPO`GDMpaq(K!%p$Bb4ZH|SZ2*xO2 zSBqYPHquk>hzQB8ME8~cDG~G(iV)u3>mFTV1H8XFFZWWGHN}Xwcl%XVD1YNjguixz z^yP7tW5eGe8BEzeIh>~|{zA`}kG~>p7Q&n5*0oBH$4*KFh~9UT`zNCi+#6)ra$;<H zKRc0oV5yXt`}%xZ52!xM6&|5Z*luE*ECvm^Mx@eZK?ddt0OZYY<oAC*0J3Eb<Uq~- zT@nA&k#7x=$Vs6Yl0aWk|Jx9CIrza6Pgd8U0$JL}VL5B80`E%*==tO;wK@<+f9f=c z2~cBzD)Ot3)Z7t*43w(75Die~UK6>(CYQ#wVJkCj@n~KuS1%qYF6&x_ZyE5xOlHrB zHj8~dirSU)gd8qMCUuRii=FA3%llqaLhxxfu0`Z1$c@vpsqzl={I-jpIo#&`7o#@A zPyjUoy}Y*C<mwA0-782_SkS@83y^MJGY@N2tC_g0qW@?gGl+L6mlBwoK)*T%WjBX< z{Ca-J?P@xc9~>--vxPusIJkRB%x0OB6E2&UX@_%O-Gh?uwreVV5Dy6tVT_Ey?~ofE zZ*B!iE3Pfad<!K*e<W*tE$icM6h0pLVVt89jQ{El?9CL2dl9!YA(;@9TrpT04pi%! zVaz?<I1x{1n^RzGordUdB-(E`Sj878(Xj3A6qW=(Z2Gf>vq821lS*j}Iphn3@SUY^ z3%0z4J5x`!uMXG(1|^(yVkOcCKYAOYh$FkVWX<^-h}oW$D4iqYyng=Pq!831h7-Ls zf%LL~^$l5eE_T$La^PM6VPm<G*@vxgR1m~`7pAd)ey6`2E<w1X4?NDBx9z@lfi3hX zFu&Fs^QSRUZ&RI8IO96tB|1!EL=(uiX4#8_^eQPmy=NQ|v6u~1givHFwkW@$5RChv zzw-+7-9VJW5YaPP>zM{tbM4OBK_b`(<8itd`owy1c<}<89&LNx;`y=Oi?%tDy;1@f zx!bas@x%_-M6q5Uos>&LZb^FpKzZLPUzMwkJTRyze3_Nj6`+iVucmI-GnzK)c~bXg zV~(4f9~Ns<i%EV@c2*bV&I`>Zw)FYYbG_wzK|kpNX=%`XFO^`WkD?bToG8eu*q8sO zPcRCs`mXmU>Uqg)CL@yZ9jq0I;9I2S9r)nUjhbR86LYL3|B0GLBYVoyCoVxS4h%7{ zU<iA&Ydk$kGTli)J*W1icZM^%rnZ&LJmuL`o1A#s&?2K187`^k97C*!{3hK1sRoer zmQ~|NGMxc)4+-hM-7@n$1LVAWv#@MnH7JIbk;mN;0*Z3$GRIXkEaym5(t1ri-38-v zxI}(R;JYhCJ-8En5<C(DeTK!E|3H>$RRM01SchJHEboOVdroFc)ym0-!^n*x-u!{U zC(9#7v<5Ky*p-Ny)B1bebua6LCP=Vuo<#smqtzM;g%lF`ehh@+fWWi?D#@SLXq+#+ z><{Fy1R=2$u?8(cEx)SsMT46Fmoeo>$Em^&Y+wTbminmRjY%B*x{Gz5IL8=m{Csz@ z_ag4mVOoybPR#zEMH=tVHjZBOy8*mGKsc36-6)J{ooCX)#b+(Mk5u9cDY$>E9PZ$j zsqGPwx$@qM*|c>s&k$rKYLTfOr}lx9j=k(S-|I2?BX6`@Btfz-P;BpkSS*Ec(~D%c zYl5(*wxBmosi^AopxYsjv?yEgB(95_&s9sKZHQl7p9SjPFNeLiS={310xLt}a4^43 zT37svn$lGOgD;Siqy6eHdu-eoXZPI4#fOD~q9CqLYGuXjW0{SKDGYpaijJ@{;G0ov z{0vF*nRZI$_6V4chcRuS7W9dvn|=7%KTP!NeFvX-wHHYtTSO6x7g78C<pOA{_dJ(! zSlhm4qLa1imk5J=an5@};^-ZUk=RGU3J<Oxw+3-bbBjde>{=?FNXsig`;0U6xr1jg z9YCBtxf`Q5&*l@>yA2F2^Q^PYa^pQ@q1Iw?@Ai*#B)OvmjK;ttfF>6OmykQ?Q)@2e zBEPB))J8B4no_54nX37Ox?dAI>=lfZ9qdF<@dU%P?{CLXU#%PfZ6B4qSBGZ%LwtuI zY&|zGoJjXVF47_IZKI0KvXlk4wo?!$P&O_@IEWu@QK2(MZ&0@LWBiu9($-!_iar3d z6g(0ZiOhm%m<Bh%TP@u|YdTf2;R1BLpzF&To*m_5QwQb*q}SIyi+uyxx>oMD-*r*# z@3(8<jcXoKM}|G-E5)B2g41-XKo$-!jiD%5#1bmv-uM6q@z%0qsBxYgcDuEiPWcW$ z&Y{@tcz24w*GebE5|JNeBI_*M`(Kt^te-Am2DRY`<7iI!md(xbh940{GO?c-86NBU zD3=!x2{)7n!wR8CY-7t7SPxkXuq+D#_mJiW5bmcqCwY*tcQ+}c=JyI~tK2I2NqKxS zIqe^DgD49J5}~p`6TXeodUr_*gbP(%2<Qgta7bSs)N81qm-1BQ{X5CvX&JowJ?CP; z=OC=~b~wO|fQSYZPTI?JnZ<u(3u48C-Y1ZJ35&0q`WXfEvP>R+uroYAK;sQ@s5O8) z$V8Y4MD8Kxe&_Yxxe%F^Ak>dzcx#{j>q!bZx&!JXI0+!-b|!CmMPBgXMxB2o=DwYH z;Un{TFNmOG?vDn|VM!kWf<N_Y#1QS~4i>!LG!}~JcumcOUEtZ-`0b!<H9QT8F(_)o zBOwERZ!5f?gYyVqVx}32xr+CZ@sAKY0^W(ksn*cppzz!EvE>YA0%ZM4q*RXmm=T^X zE9e<*@+_g1m}Y0QrnJd4;7_4{WCPgwx~U9E$|Nx4)?%7Q2&8=@dgpdYhor=Qb)<dc zYrvCxxU)uLoUeTeNK$@|_1B)vey+B4geV*HQsi!Iz2U<*$`%n*%nlE-Y;A`38@x;( zUvi|Y2=-fClhoc&z~6jY_>t?NupzYvG_y>G9Zt*Bpn3v3zWAEq`xx70*-S%-MLfKx zrx5psA90xzhoxk>@nmkr4S4do{ew1&9#>orzO`df_psI4i!Vw*o@(MK-uU)XG)^=$ z(M)beCiil6#2igaLRyTvqP3s2Og<GNzHVz#6nvG+VJs-20Ks0O7Wx85lLtA=b4yjT zhT{0Jh79yanPa4m{ZlNpg$~Fl8c)KT!5_C>o<PM2=>CF!Kb+FixdSh`5IUE*U^3he zTvR|zYLtk7b?+;*=ZJLa%@2jX;`bSvv<IIr0JI@-5C?gOg1oTfKxahkk9<!Le<Eod zZ+>%l&c4>(0DwfxQFe*Z`CsdEkeBejRGv$e+5^IWX+nL#Azu7Dt1w;^S(}`$OCC18 zB6IqGNWP3JTO-AbJ8-FD0yWFNyeyrINuDxy^J{qDWkX{uK01Iev!gx1*@xzeRK_qT z+>6mZeWo>Qa{TML9=H?a=ljI@E)DA)v2EjMieQO;p6Ubs^BW50mj)zk$YrHtKn@Ns zEzA8RS9gNB$jM4TWse~6rwQ(L<`0OX_fa3M-gpv+og7~xx1G?-+Kb}5R<76^FpW$a z#@wBbvAE)G@-BER`{nGcafNR)A$o)LP67}W1{}#p_b*`(rbedLy!-+PfmfA#{hCN} z2rg(aAF9FX_ktZxY3Ex(G=*M;P;OzDw-ylR>kphR;HgzvMELAZbtHZ5M5I1^J1Jx} z9O1x)SyQPojE-qo0}es%Gnty+d|*%absMNOI0p?A<}11A9RcEMZtkW@-kLXjE=?vm zFa^(VopQLq9!8-G>@y!tEU%3*xj$tdF)Mi3FA#{A?W;4>M#&YfYzA>Hc*Svb@fo0U z_tQc99YN2TPhVKwVd}x008_RKchMlU(|+6DXhZ&@fPFgv1UxrSrPy{v8CM7ytmnvU zr9896{KLdjB|Y7lLS;HMGK}nf*LhM-s+WP$q)oQ5<SZp$_qnm@Vz(k#*6B}ZpU;IK z0V-?`ewr7($L8-Wn8SMoBrT&TsP9GLEpkzYr}Ve)W?K!GZapV$xy#m+THa>_7b|}E z21d{yhYu}bDF#^#wknWok%wmBjGEfF;W*!IT3<;tkXEeKMnW)6skK?Ik@1DQpMkXz zx)(x`Jgp?i9B||Yuu<1N7o%uL%Szrnix$o`QZRLYT)YKb=d>)bl`TGPV!zw-Ewm52 zdI)&VyJAJ$ltq}M5ccjO9X{iL2x{=!RnRwOJb6#r67r)!{T#t4`jZMTR=n#H{Ve)V z9x_~5iuHbK;PkTK;k``-`1Ofhym=`)fW5~vO^A&Hb)P=xEh@!)>=xFz!n0Gan{Yz< z=1BIaVC|O(_je(D6fo)hLW^tP*Km`esuoA(I%L?8LZt;AVea@xuJ3AEslcxFjC1?h zp*A2<#{Y&Zf{!aWsSCQMBRt%aniQ$hNJ_uZ6vp{`Gb!vN&hV@|2`UVKL=v=02k1lo zF6#*;^OL-Ct_%N9n{9kC>H;e<mrDzOR06M1Je1wDyPpQVKs6tBB=+#4<Vgym!-yI8 zi2XH0^365%oVf}q{;3?RM{u?g9_ZP8)e`tFCwN0ALb1S^!Q*1r$R#fdZsfs2VsIx# zvemH6%i)h$H0$HI80Dki-(eEjH}xj?5I19!@FJ({A%ip>s|$nrj+vbhw-D&rS}4OU zJ20_!2z)T6Nh*ok9y_%vc%hMwK^nULJ%8ECPrXx+(}8um#JE^W>-En>F?2XPpSQkt z2KWm>R`BF@GG~3&8d-$62};f45tiAsVq0HJ34-|Yu1U;!M~c_##)NO}RBZiUU{i<2 z8wWK)`_lQN<vk4Y+?_1<yn)NhPn@(6`OrCb(&BN2jaoneS8aD;W0+io7j&l2epds5 z6G?4&A-Cs5*Os9U3b4PK=KT@RCCaApfSrTKc);@*b%ER0sS8#uBJk{E6VdzXaj9JT zbzUeVim}n|Fe{WjdpZ&Cn;pq7jFI~gw!N^R#r})F)scpq{<J>(2_0G^jHm*U<(g?# zpkUhE`h=|OE%)z%iAML|*tRpFc6-fOK63GXoRj0nWr%<B<$a!lp(z(4bDf7$?=;ZV zkV$&{+Dl@ltML@k`?=2ax^{zr>i6j&|Gy~HE$B`()0RCR;5WggzZ_O7paFgN5WWIQ z#!lGYBo1?GKrE2NK>AXFX4}M&SPSUA7*22!I^8`91()a*y6FrMoekI5kBI%49y9^V z)FX+z=zD*VrThK0QY7O<uWM{Q97lu#&(c<S>c=J2)dc05*)+^0H9p%p8O%Eb09**A zwoZWtRD!rH;ZX+v8XCH5uVhX<5N|0bFc*M~Lu~U;;D|670zdCXRJaxpszHp|eRe|@ znGwo~BtrPgf<By;&o4Jrx&6nagNJ~8eDSn%F)j*?&8vcsqy3~G-zY*GD_%-7(^aFQ zI`$$v*SF?Idiej?d+)fWmaSn}MMVTf6h)<kq9P(7AiV@d!A4W*O+ZAYNs~@Qn$jX& zq(ntP=}51k2%!gQp@$wIK!5;ANV4~vbMAZYJ@<RgeV%i^d;fU9vVS3aXJ+=ynzh!f zS+mxff!krH7DcXKMuc9-*e}tOl78$c8=uXNmVf23kW$l1xXnSMvsO2fZ7;4=wvlj_ zz&#(qQD?rNXWK@4Z|&dfQfu=t&dYO#&)u9XZxzA+;6ogHyufw7_F#sLyytIpCn-cQ zJof9E;^reI+c9Ovrc>PV``R1wiuT5_A8JY}oP{l_D!JBkgtK^M-FUggJ{%G%*QU2o zB=H?>{hp0gyiet1W3gpWlK$|yZfNKQV^%)%5F=IFR~g-`eHu-#1f$+g>fc=Excrp# zZ2u<WlMva5hE~gzyX9k(pLkrtODcISyr)sl@yr#$&*_b;gM3dyBkQNluJi_}3R&%V zFeV!7Lo#lAPMtgbFs9?BwdBrpw0zN4ZEW$rjBp{}z*9E6vsW2@D$$QU@SpivN8Tk{ z3NNio-L&o9t#huzA}9bFWxH=uY(k3X+}Yr;XXi^;IRQ2;*ZK4j?Io=Vj?L+vscRY9 zhr}TjJPPOLIBr-e-`wQ?*0g@6J{@$)@9NEdtj>SMnO<`PvPg?*S&V)<ulO#ncEQbs za(Tp1G0ga72wv`%|C8{iUAm77b4v`r=~#bl?6_Qd$E*0HcUix_S^thNR@J-t`F4QW z@fi#e<}jqRTUNj^X%4-P46tC@)P8L@Up3Q+o(_<aAYXs=s`!}Wt7wnp^FfVcPdQ2U z7VOub&F|gGAyDpgl!-2qah|Md8vO#PTFh^zPhIl1vy{Kw<X5-nO{d~e=$fNp^T!1n zHffxO=DbJX$Y?;al<>)B4-I9iw`ZBjd{<x`zoB5|eXdBn&Qky60fhJ5K%jp)0LQz3 z#66`^yCACBp(+C0QGzz^@*|iH36tWKiqU~en5)O=y9`ZtqcC+l@XRtv#su=!`t+&0 zCVkdmb&a*Dg#lClsh}UY!y{kfcXC8-ZmEb0-0g{fW%f;7rt(U4kHE5*LBNIVt;_KN zbs%2w?B_B`RRA5eKYx(f;<E&AMYlcEV|9Wf1%ExY+4|j8ezqnqSG^O0UTn<Dnh*WW z`L<T(#P^PSsHru4Aya>}Y58-x>19uC6ASd`Lac_y7Ae~R#zTqdFw?anvMSxJ%6TJP zASK`MxbS$*@iBkB9=La1E`lpn@cKS3uOPp*_}COG*NAlS2u1Ricz5}L6imRydbBGK zN_f3dn0hEvYghmZ3uRg>EJV6&zE+rP**c+GrYzPALpvDeq0I?ejs6M5RmW(VciKnc zb~=Qs#~Se_zK(A~CqmYp8&;x@FbSzQhxKW3n8&c~d)>1Xa`3ejVQl65Q<&7OWg+aE zW^SIEQ`QpgXpz&Um9w|m0^i(GzJNunvaa<n-Z|7|AN`03C<ebz`1DN@{k3Khffe$- zA$t9oa<agTCMDaVvKt5sg?^q!FRi-xK&e5x8+g1#Ws{~Vxm_yF8)EkC)MBBeh2>4j zUbMr`HS1yE`ZcGU=p%5A;Bljzu;we#s)3`baknHZM}&)Sox4jk4On1vC~P?)Gq;lC zw12wE|I+hQ`wcoV8-ze+1qrqHL8|xfiJyZys*+(hwKKTZlOU^`=WYncu?N7#*@HLE z%NKiVKx`gG68(KosvlAkRl6ds;a>QyE8-Zl9qDy^(!uY0l@8Yop6k^S*}>CwB09Mh zpAg2|Hv5^Yj|BS-3LdH#6!gB<8^aamkT7v0+<so@$J|?(MD;=6GM-FyX!*vj+|hF_ z@03`lTk3o`h~%2U`N^|;H#<66FI%_<M>M|>4DhGRs3-cpXz#qseXVAY`!&V8xxKal zCn)FRa{jpQgF6+z;y?CuWsZk*_YI?l<F#)i)>hTkx$6X+4A^~oKXo+6irPxHKDii{ zlib*HTr)D2r77_BB#{x5BQ_yXRy8L5ZEfdiph(+o6=GjXIf4tV&ubmK!xveg8`AC- z3upvEW7oCTCkB;_yUPO~KDS8lG2CBI@Q!e;JFR*l?2ybjhfhp!{rzAk7y276hH>(9 zmd+SQa|NpwXSz2o!9R)nrLIK!OO4|ZCSz9*m<TOvy4-xQTh-h+q$oi&$F4t8_I)BW z?k!#L!y)QP+p(h$5egba3`vuJ44<UacF3DqHgdNfjPr=bH8uBMKVJv<^{6d)tUU>M zd!kz!-I+YB7Uw@aX4L*5)zj-->FkbRY)D=@QP01+_d#us!0Cr2ENj=|Ze{EC3pC-x zi*J6jefieXtf#sN+@;HjIzBtN*bwF&d)4LTm8Q4PBb9-#PuZfsl@Iq<d0d+lraC-F zcvikB>X}Y-^p{mmLr7>hetf4Mu-J77U3jN^Bn_eT=vL9_ey%_{P1W|9x=vJRAsU)K z_l+p(Y{4zMIZ0U+>otzcVqa37O)<CyMRIz~X7@iO-j9TOl7FC$OnbBLu9}<^?*3G2 zIIi*i@J$vjo4Od&$_mQrqtG~BUDMQFIE%}f*Jcdoy7{5|tjMFUBl3gMqCO^X^;A=l z(!~CsZ5*hnk@@F*>3S5fy7_I^#knq<rI_OyU3nie7DuliG{#=1t%Qr6{NcT>@zPbV zs{f4fT(Ql^s<HtJ9tRzzvrTR5sxP?{X!4&22pPeC=Cm%QJ(g8$ZZBa*2FF&xs%F-$ z!S>xQ(S5oj`Q6ToA|;ZhfgGFTx0qFvI%b~ma66md3R`cx_hyi)W}|=t11X-1JZ$dn zhM}oKS1z@S@tWN5@;~U*v%{lw+2W@+%J)8jO5$}YF<;?fIWu*sCi}TXBK?Na$IX-0 zgvygwcDI)f%drN@O79E5P$U=Ma>-xMi(y$^*q@Y1D?&Iorlb+b?dA8QCoLYk+e&xr zv6>#eI1ts50?ZzTzQ=xuKfX5I+DkG7>t~XSmdx6Oe0+{AobFUTe~|6X(!<MvXWFvv zGeS?uL9jZyy>TAtg_*%QUHz$?cZ(j^ri^Uth`#K{SW^y&d(?hR@9hFe`mzWG!{8f5 z1LZSINtgXppq5^ElC^)THdAAvT8Glfw0gfpT`X5Z_<4VkxogvS3e@MmK&smJ2sQF? z-Pj7CzZFVYcUR-!$|v3-drRv2I`V>*%@4ZbQ7g8_$GA*RT4duqEzpve{8+Mw!WHP` zynDjqj}He4^ymt6J-z!>?us?Jo}Q$LJ2~d?<czTO#93o<WxjWqz1NM*5{zSF1w_)( zi#B_Sd+h+|*S?+Y5j!ZHERVdtu6FUwLi+rtA5n*_GZydM$+Zb+xssNN_Lf^<X5S_h zg7DPY7Pt?qP-&V%gl~cU!Kc?2FvtYs3&v;Uw84ITS=t)q!D%(XMq|FQ_Ds-^dz<%t z{GHS;oxbqcLI;YpPC{^N)Foe~)Y_{74AZPmz-AnRHYW_FwEsWg(Kr!g*7D3bEB$0B za?c*7jNigUnJN%!|89urFYhzLM8{N|GSp7Dx)dbzoX<?TWx6JCv3!f8eEb&Aqm%Q8 z6V08H#43m!uEEBk@W-doY-|g&2LSk1ob9zk<c-t!F1)>e>E8=hePla8u!IgGk+2TU zRTRnj((R6$^_T0=A7Q=FMF^1uy&H|PVNJepoQm<cEHuo-e(n|`1U?hYhZ&L<d>g{p zB7Ok)2M-=50$nEdk9P_mL{TxA8)@3YI2&TsM?^nZbZZs4WI%|}A@Ysf1C!ibcq9^5 zPFaw43i{@>=IaNm6EQKPNEHB)p3}2!vqdIQOfILSbc9cq+{F%7Od$PNJ>_{i=zZ{= zvgr%E5~PhhaXq|q1*~QkhkH)LYf|et)<E_7rA`bkC$XL@BfsOkbkdnZ7=71vWDK`? z71XV$rH`A*q0wuO9$9Cpme8@SC#yBZ*n)sDl(iEIme;pS%JiRvN7`MEFAQ@@7prl! znBBCCjK;gUQN1Tz>IytfgLaD7CWoKc!}7N~Z39VR1f$GtC_pw`1JVdADo4Auqvk*1 z>M9_&Fa%b5&wG!2n}i{xL<Ssu(lxnJ`=pqjR7l96kKvBlq<A8+CM2gSDx(GhH~0xP zJqvviJvmPHgyfb2*>9;e3I5u7JykY-02WbG<GB$-gq5w~%HHOn34uhITOM`MvaDc? zjsXFD?i!#nwp15XfR1>N2X{Q*TvhUfK`-vONYHgY$c$UA2Lqb7ieS{5I<W=SM)D_+ zqlqjmdW23;+Cma;ts*Gq#2eON7nmwxLn<V+ykrTXAc#-G0f=!A4ec#d0!!ln0rlFL zQ5bejP6l)fyf=vStzWx73k#>ZiPq!no^Z_~fV!Ab&}$YpMMJwzj}YCa9ScP6QxP^a zPc&GHvl&OTKcQ7fx01k1Ze3vbdvL@qMKrRM3M+{M3n++=57P0aIK!XdRSs#e#GZQ# zdiw*l%D+b_Yh9K7J!ou34MF+Nfq0gvQXnN5AYeS51>zSeh-h!qmzerrn-V4l10>#E ztmh&OLt)=GFim>bIvd6a;@<y{;rjXOe`vV&|72<A=M2?D;nmEu-f>i4e-Ru#b+O_$ zzo?fZfigwD3=9i?{_`Ue`tA7$p*}pv7D>I7lSp67eRuA5?77>!gjQj`Ro=03P5Z)T zJ{79=hrY^aH_(QJYpaXQ!k}jXP>3Y{0b5o@x3>eoZxf4Pt1A+}y!p?c{9&%;$`iSq zn1Sz%{%8LVA5;G!d^Ah4?AgP{RJ1EMk-R8<i~kw7{-3@4PyHbb!%9bP$@+b8&brGW zq1Zn_&N%&t#s6X=L*o7{W_hJ{>}I1_f3MW#=HBC%=dY{R7+>pR>#9|G&mQ#R(RapS ziGRA<M_1;{o$b>#Ymk<S-W_ke=)_u5{pz6(Ef{?pWBKR5N<jZyQMteN&Ndn#<rj=^ z{n`%La^L_In()})aFKB^<o(|vPWMlD=D#V(KwO%0<k%Bl6yZoeOYfVn#{wi-4(yFp z`gV1Hu||+f$sS%OkuZ+PGoizRNA;73PC;}Bx%1zN+&Ud<^!1d;m8;zoy4=-Ar1Ui6 zr4ov`W(}@#$~tyllY5zaA^vUIk@bZL`nf7M9!v3d1v>-+tLZZr0a4nZ5S0Np%A}35 zI|072LJ^+6m*%=CYS}O_z<EfOAM|UE0IRC1(yy0JHzH@LD<dU4IuAr2JZEzJybLb> z%)lpFt-BGNnCwqly!j4B4R}7QaR615#Lb&pBDj7!$c~hD;w2C$Ry#tg+aO{Ffrhp( z+_Zu?9P}f5zQ6uSw}D1ejs|B0R;rc^{8p#Z$mj&Q+0-%!TG@*g2v{(cw_Rz9X=WBI zW|w8MpdkY0&aKJ6;N1zZK8UO#_J8H8-y1@?n(nn_M@9unS@@D4kTDX_*SqlYSzpgp z#JH-F&)Jd(*}7&Qy2jvl2Ixv-Gh%DI+l#x-SF<)I?XhrmNX@|{`1wHE%gakj>$?%o zs7y=$<6boBJY^Wo=d>s9IZ)t)JBS4LvO4>Z4OV)-<W8@fHJ3+9yhSZ!xvX`ap^>In z)daflY_FwkbT=zf*polfksBIR9~erhu*n~T3`w9vfIVHvm4<dp81pt{t@SO$j}u+% zb<&q`jr!=DS4V+F<sBrt-u4ONpy^{yFrd>1r!NX1`tvPbINKf2M^o2luvIu}^NdBU zX*DyoP-}XJ+qsz;N!!1m4f!EdXt2>|pX7qK!8W8bFBB2Rs2Zcpj+k6(;wn*<J#Ckt zP8yWcOWk%rH7$2IDEW?B3G@L$)u1Ptl41D+$7Rv7ih5NziTM$;Y<qF^<QHeE=+U$q zW?>21DP~Ku;vBSn_@&?2Q|hbLP64|10O*4#2|j>+sWAxCb*Ce37T{j!HRN<j6*!&6 zWXNw{IMr$jM48KB;VLNAFnoOtYck?-5s|7S>Gt?Zn*Y>R$!i?X)MR17Y+lkUI5<h{ zawuLh>8Xh-a64%Skod?)RJSs*vmcV7Hn=t1dOaK(%^kl_yw+W&M$VyDD&$1A&Qw#U zbJNe}pdL^g;<{=lEBXb1KBWkZ39ticQeBiE=0oGms#KUY4%9X^{aVoT%o6P}R8dIZ z9h+YE^+kg!80J(mVTR-xe9e|W-G0q@EWe^~+rks>O(zhIS#OedS9g9su-wjDn}~Mu zo$7*4vs2h3fO@>Iih@-e6#EWVF97<f1C&r2vrdM=jXLYk7P3V~vdxpJf$zYdG3Qu5 zWHnyZIvpD5t~l|)vf|;@4uz}u^EF4F7O{zZ5i^Ha(3rRVV}o*XE^9t99twvUCmNgQ zf1Ubv{cOa|DixWVoNjM#(@nTTg+cCbggy_kxBE`H-M)+$G@)=l-$5VkOq@7+DcpQu zr!?=Q6-M*qCe+NI3x&KGUNmry*b_d1;xeR-p?1G_)s{TF{RH*u>vbS3mmqj(-~IOB ziMla5BEg|=Xk4{bViW~4N>2ZbJD3Wp_XKS$z%so1ZV2ubkUVntlo9u%n;eg_4A2{* zr~@@uiF--(S7WTs04m>emWL@TJ3FQ#P3h)|<GKl#A5+H>`H%I)=k;W5jVPC;G#YRL zo6u9&c9XuJ3XW$HOAdD8GA@31-(GjsS0%&hQPly1@8{FX!XlH6sok_qvzuto`oXm( z)hZ#J?|l9CD3<p~*EiQ4y0b1&qQPs-+m^iJ|8YdEcrrc5=foM0GvS#T<6l3On}r!* zu7Cr|U%u~e<fLJX&`9Bu#(I!|Ufr>KU2DvafV<)z3Z{c>ANbe6xJ^OPoP$FLznC#; zRIwOW(Cx(*pf=0BmktixPpmS0e)#@8XE3b@z_j~DePF>;FRK!v*Z6PA81SH7%MZvf z`;M-WVn>57ro2=tf|E=RiijQ1;}Uo-*hhTkTfYouo!CZNXeZF%uRkXkyNs&!iGTLL ze9YF)B=}1t#S?f-frw|{0o0#71XYuJt;TME6a+1-rwvfSY_H*AB%h-}t~qmvJx~%D z44p$b|BlHr(C3I>=AeE}U2(DCI*YW<9iaLAin0iWJ*nuI46~*lM#c*kWkyB?PvFAs zb}-RK(7P|4?VIQ2m_f5+jDqcRw|z@K*3HIvjC(G(!@TY(>nzKmALszhHZ$J$k>2Sp z0)I;#l}5v8w>KfueyIJPmh8P0=ny%II!=OJbEoT+{>JpgBNQC>CR%O|x~mIFPwE10 zc#fov8||Ri4_jruO(>rQa?-zCzxBj<aF!z;Y#<_pt^+6`@!7Yr;KrdGP$^g;{UQYd z=cnrkf>lLDiW={v55D*9;wQa-GzU}utdjn<)5VVM>m()~9KC32t^8BKA`V<1j;9Gz z;FnWkK_0TtTN)}#k2VI41K(dm!L0ZH4r7dA$$${98V^Rw#e?HMx<H)sMX+7Y#XDV@ z(Q2EjPQZ;e(Ce4vnN4MH2D~w_blL~>JA!|}IYxo6I4HBK`s4w|qgWhhW^ci~3E}$` z3ATUSz<zF6hz6@ZP~4$h=`<AESuoi-Q~{_-XVv*d8vGqGf1@Bs)+=_sh_j8qcjl&$ z(hK8Fh{R>Fw>~0a^Wo39jzq;CR;k$WY@LIu%KJ}jSe8)h`wyCAUQUdV%F|BlV&8;7 zE_ol&5|(KD{`r!LE!D&L$%6+(G}}9ntXqPnig{X;#&Nxr%JAMqACPHQ9a3^^&^Kve zbIk!|_E6wl=gs#YUP}M&5U$I)4A$!Q9~kqF_4drV<Fwj!g#w3szwq(#W8I%|7n6^7 zMrN@-!`imPYTt=UmEJt^Ju|P%%{8b;87Zp<%mx{8Cc{t_4?a9C?(<Lke!p#QF5ROI z=6B2F_Ln;%LkCk+i~6zK;Vdon?D3b5?-ygy69iq(Tb<+Q40#Eaur<29$nu85=_Vm^ zX&`l|<mmtAoPT0*2Ikzl!M$aG<VD46w=Oaa0TsW~uHO7NO4FatVrW+r(=M;CoIdyn z<;I`1lqTdJk}l-FUvu%$Lp}RL?~a8FZYp~M7meoEgjhViwQ_zrCVY>VDV1(mHsY6R zVL3SMH}!%_-5m`uM0rcZFpsW8lc3Sq!IwniRq!bNA$4`TeoAb@-rQpV;mL*Btz^VW zfn#*^WlW3Pxv^?sYE|aBug{KsspmG#w|kT?*POfp^I1JgG~@bPC#0`HE1i~+UL=|w z$&X;g)x#WS+~6j=u5REyeU^e&r1}=Rm{X_x(vGm&=g}oBoPm;(a-83-UgodhOeP+( zP1qLwTCpo(Q4J)oAQ|^dAOsGsVuQ>!UrW92OrX*=i4<r(<TIAl3t;}G%~!V4qzXop zPRrma6JmKqbu0e00k2zQd)3U7m**@D#Z!b1!w0K=tkceWK60#ncGZ=yV%+d5+7c-o zL+^TWQz(f#?E{}%hWJlXY6b$XSB=}P4Zq8Kit{{L2#Pt^Ph=59TjepA48gC7nZ`d0 zu&kVeNZe^q?lx7&_NG!RK4ukGLG1&Chxou**_W%nLA&Oygh1n;(P)QAkXNCvv*htr z0E58RpK8VX1JG82L&1@1m9pl~c~46V8|FcV>rN3lhAqtSE_?`^jq?2)A}!Rg${F4h z+j^kK12E~z#U$fOO+V+Or5`~Z+uPo$FU5%&PvPXpN8KOM@0NVI_f<YAL5XWlEo=aG zFEJi=vCG!rt;&p>LT}?X+V*zx9T#zFgIFeBGngaYIJT;*VApQv4J4IG>@+xrkCsY) zC|=@uS)0ItO=04t)3AUF<EZ#wt`o5sQ@L#zH>Jx?8I>|gQb```n$t+ulqH^7WyN9u zW;>|86t6_aRic7C*JvDl&Nk8{qJ*TfH2%cdDc)Oa2w>fHqc?V5G>@pUuj?s!>?9q> zZOa!Vg{Z?<ETn_YQ~?C{fnS??B0Eous`|+2E#rfU>kM322sWkkT$KAT>?T!pVJVqs zX?EWAqq+UdvZ27b`M`oQc2!X%&f$efyI(lZl6w3KB^{5{T!lc8hS6K!!tn@GlB6nP z)J?Vc!n5<5yPPFuV3);)x@rn2x>6#{4Z8WZbjGNn-bmIY(0M2{#H6T2H0Pc@kAf|W zTm;=Djj~)AE6cv$4Po-HaQ>TLe_L7Z(>r$ar)AbBTcglV=Z(TYeVT|2kUTMPz={t+ zocdl=#NGAn@EFBCZ)|(hF92VJ9Xvg|nu4KjkkeGT<Nz9lh(jZwu>bJ^xl2f~7$^V9 z(g-NHjCA&Knf+6B|Jr^oSijT~^(P?t-_RBU)T}@30G_IYw46mIf{(|}f*!Q&*`a@e z`ajkY5VOYP3`p}Kou&5dVP5(Nb-!HsHwpXe>kR6qS~+mr@wQTnA@rY?YEt*hCb-_) z*L&rJR|>pYJE2teS+V!)bB=+DSADGU(^860ipx_6Zo1a&*XFhNFs&KtzbH9)_yV5u z9>&8$=Ane{i>u`qN;_Yaz5Vdb6f#LFi&I?9ysem2;)WKYyW^B_d5a_%R%r|dIeJ7T zaHAxfKPeXCIR5ISsKKkH^nnh(obz0R)<+XQkNvdihmn7J@J7!zc$m+*Dtx*BO@W$Q zA7b)Tqt4`*%v1w<iPzV0mmQjD`w5AJ)DzIVa%}3WqP`b;*s^suA{Y7wr$&mS>tyL+ zDQ7U%r!-r%A6?4*6r=O`@{nzopC0r12ajZuZk~`SMn3oiz7V3@e>jM}bCGaZvQ@MM zXfd<A8t++l`SkOniw_r}<3DCy&Yh5ytWBA<jJbWNyIY}NZ~;bMQh4l`4kq2TE<Qa| zoUk5MnX@<KOjt^7Kr|waYyY+%uC<<x9tKfh&NHhz?mcZb^TVd7i;c}?4IM9a@zmzU z>Vq`DgG*+(9=g;PBw#BN?$s9(cZ*;2%00;KT(-Nln9f1h_s`X0L>}v_e3{syF{gO( z`in+45$`n3zKA%kF@~7oO`FgJr!U0i3dS99Fsx;Fw_nfc&P>P`a@1<vD!Dk+aZyKq z`XTQYVkyjGfWvdPk@+DIG8Xt2pYnxJkCEh9ci&}~V9EGg#PjuR@9J6C7RAWCb;8wn zurwv)K}X}I$ML=;4uZz_1k;fTFiAl8)bP+@dRS7Wg~2VfM%LcWP$OAXozvi_GX-Cn zB(X}*&ZV6z%{$or^WCz}+f{@0YfR=kj~?}uE7q#^fL18?@WaMTEP)pm_I1Cz%U<oB zf*?xB1GZvp8R*<xbH;f*u5sXy5u%E((h5qKDCsf-%lco6eU&nq*#=#^#R|4xUk5uc zZ$Vajp(Vk+uu^sK=TW++&Yk)w>q)gnnW=dxFbVSoTzf+}rz<x6{!-OG=Caozc@rZ9 zpP=jDv}Ux&67q6ueO{o3K#&Of6?S=VP24AN{Xs{{*G^UF;Ow(cdYtfxP?PE&v3fi~ zZ4pX~Z}CR{oWzYR6BFvvy?5a^Ius-^ooAnBOlMk|-mN`2g@dXjUpHV1ec<-qRp$M| zaTJtJa_AVTe37m@Ip0_6oPkLKd}TvVXbL20h@_}B4(b=$J(n+Ahm_lK&=FD@Of;Y( z#X6o%{v>m#bB|LTJ-~G=_BsDY@IjRm9()Klr7l5fPt&t#cd@aGXTy_Gx1n!K9WSpv zV%>%ngW34l^%J*F#pWD*3@hXzjKs1&sAV6xt2m>*@5AJ6uz3Ys`uxLH@<*MQTPjyy zxRLBZF~q|)kN(%TN)bxKwSXyR&nkY3Z28joD|kW~l)zuk0#DQeb{{^0Ps4x=-d*_1 zMd;|SKWc<mz(}WI@XSOJC_QQg+Lbl~I-zvL&!bf6nH`vh67MRQ`V{6z53pP%0dpke zL|o4ftY>xm?CCUciNW7%_ZWxfslqB;CDg&`Y$6?zPf=M<M`8%{b_!Cdj{@i@teK|c z1{%hFVI94o)ZK<U3tSB;n~lK<<8zhzOwz=Lr*AP}m&0!${R-b}8aglTY44h4LEPet z4A^lMM((&5ApBFH4UF2}AWtKizY}jAsS47UhpL1&qRMBN8Yezsq$qQ<&^8c-S%9$% z(47^$4#R;d-@HJ+$C7~EW?|=%_{r2C=y1D}NN(gCF{S8fcy^$~ntzx~Z_wpdFw`Gb z$&y?q3`A+Mdk@p53OUChDzCM;he7~ojLsT3B20R;IPccs0nBK6tusC9&c(bSM0n(l zQDZm3-uBzIGX4i3eure3m1%KyOGwmvG#E)WlrV2xZU!52CL~1He4ZaisH}@e@}hIk z9@~|qLq49PDUBWNgTXoGV4$u=RT3Vq4BGkmbOtD^$bR#)hw=ph<1&5$>xC~ih2&vN zNH3iKRPZ`DK_Fy<d_rxo8tR}>(nfNlTra8XT>Wm>fperto9*7xoInX@;s&*2OdjNW zL1SBA?bczmB8jC*OmGWBe_SH$Q{C>iJh&OsB|Z;Ia0PNq!)CLD7m=t#o5=Z?iD4u8 zFyC}vh$rI!&Lkq3%v$+YeZFFFGX%2-k4Jv|rD<fb+kX21x?JAG4tL&8mJ524v|g2u zdU)iVXT5Bv;fncb?^&r<fQ2&{+{Qx~(AubiJ43uL%2nCARl|M5#J{-#qs>SB>{iTM zCM<pSP-gX0<zXi?PBd^#g8&th@k=A%PKUk`l;<JdzXtV=*2;HoZ7fos<j?AvU;JoT zX=9*GUOT<+wc%uKX#Z@(^kF(l@EX9rG7+?@E-@A8FJU#Mdn7vt(hS=W<B_*$b_TCT zZC1he8*YTJ2ULe_A)V-G2Azx+iP%EN;j(j!$O@x2<PAmh!&acq+zu^l0<JL(YBq6$ z=X`=Ef;5`cfuXP)rzq+Y*AEE<ANwm`<Hi7U;%KL(bj_!tJ$v@m{$tYr{Ng_}>5ofB zkE)#xwSvHfzH|wg&7BrG(awCU@#HbbYOUlR&0(Q?uio1_L^h6^KWPoHSQ3kz=K6q^ zHrhl7{{jHpGs~u|(a<a{`prFiAUE~t%BS;Jab@)*9=MlQP&1H(b4yD}>WwzgWGU@t zh=A-Mo1w6Kcv&CycyO?;!Tzc2gq_}n?0d5`W|B`FCF8L;o*YC8Bl@@)gL2*Y5~QqZ ztO60XRWcM5TLsV&ZQu(6CWWvm@u7d$obaRzlpWZp0~>w7t->R}Ate1R?%Ep{l{{{W zomL^kp`;JuhNUNM(AX#dZ7~@{S~}Ss`v62KBozbb2S@_t?2=Se4*|dEC6Da}_sQ?L z*Jglj%eAzUh3~THcF$pge{H}V@=zCe@;E(g)(OORVZj1RUHGH)+I&!s$nCrOlM1e7 zlEHmz;6EH=Qx8s$QA-Lc>gpzDa252rNj|Tm$|IWL1CgtAN}@{z+8*@MhAcjWh<tza zoPhi`OWJuWz<PlM)TV%us=dWNx8M}ybH?vf<@7OTpIMo6wyp$pP2D(gS8bl1(bq5Y z7&yVK7zWZ7!nOSAG52s4g_h)x8$MO>yWa|tu{_a;UHtbgzc~bnoL~W4N=%2-h<wOC zyu?HwF$Oj{Yv@DIH1$#4MElb26e8X4qBQ|;xaHDzlG75U4NBM$tpobRR3H?bN<b#Y zvN{tdZ2<KEji0sJR0Z#T!*RB|XRUp+G0DxD@yWk_=>O`O`zJ@;rW>8v_WEI>>g$EZ zJ$num{DY5Ehkqm6zjJ3OuOCcx_4W6J!ezFHf(ljQrDbp0X!SqLdUh(>oykE%`t=^( zn>QYJDSZ?aoJmM=<tXFi+^3}{{`U5pDc*J217G<q-WMPL6nMt>#aZNz!Xdusah{`Y z{GjtfO5I3a-Hr*)(!Ln|B<L@V-v6QS=d`?dFAAqGW#$Zp6Ti42oavq_&|hDiGK7Dt zf={4ltDs^X0Y|FuO9z4Nd$L&UJ%R5}r0eanW<I31bp*5Zov>*iuTNlF@@>Py-Xo-7 zw)S@f+VL}Ox58b>w0HiU*i`@Zw=F*=Qc=^sBVQ(N3oczhUDFA7o!UAQ9^TR3t<q8u zX<h!9$Hvs^q@;IJ@-#=s?$nhq((N5Fu&tJ7Jls-dp-y7y2;(qXth(c>$Bfj`827J{ zTjBORRTDQTOc(3esbJ_2d(uW)?T5_05j>N^BNa4F;h`(aVy_ByYTH-bAX0?BquM|~ zM1pP94{Y#+Cx69(!)uL=Pj1m!KNkiO;-k7Y-V@0lpOwAsgN0d`J|gL(1%abz;V!I5 z&Ig>~s+G~g-N?3p5H4w_qTQsi8;EE8Glw0dS{ru8iikSHPaJHV)r@4i*xGGB)L`Ra z;&ghIhdy$Xa22#-MO%L`jux3yM&E!ny&FzSe)mpma|8O`mm^XC+MNEIoIw3n+0x^$ z#ZMDqda7}%y9N~YS7y{$WP~JKt3ctsa)jgo@n%j3>!*+Q)I?7<;W^0(&8fuvWahJD zOSV%k`)cEsT_CF?+qlgac!G7)<L85p*W>{qK49FU^^9Dnq>a6Pc^NHZS8>S3BY$U8 zo2)r9fYrC)?Qwf@)3=I$+@a8!$MUCE%|Ty)JMR9O<?19mR3<SMuV}H(hdyQl_3%7% z;&5Z#=?h6shA6ApVh^Rm2ws2-c|<1CySt7`b1eCOmJ9BB1vk`9Th2-6bQ?0jy)=Mj zQ!1ik%6E_ZrCzDpe}>8nuZRxO9v$lUI9QZ6?+hG;Lgk||{_rb9c90BZcBxkUX?xiO zY4MU|(&ki<`jw#<2Gh_Rk0%7(H_(+zVH+FVaXzUa%LFeWFLF>>dix>#+iY!V-d<Lp zHl*C@^2&JnsLY|2WeU0=__QD{Y)08df#7!~sY3IhRI?ouB74I2VGomRdM&EF&SO6_ zqNOwUP*P0)d#TB=kbY!|4IoR*m)mwXs&Nc(uAupj6kK~`NU6wYGQ|{Ft;WvZ5IbyV zcjxF>e{Nf6u7EW0c#53mqsiLC5do8u!TT>;X|DJv;hYn2`xw3|<f_kvEZ<5!K>BVT z)UdYc+)t||sTu6;?(R0La!tz0x=>(|FC<qHJ3Q=mL(}jWmtj*!Z<<5+8^2B37@?RC z-4T1!83XP_<`KX=_(<Ln^9j(Sjo~^&T9nxz8&08hFYqYew6PHq6BE07^{TF^sphjK z+f5=&NHq8L+}!b|q$g7JyD-K88ij(ZDgBXEmUsRARXjW}-}3T4efbi;ylgV-+$eef ze)O!0$ha%})S~KbDh_cL(mgZ6@k0~5b?sWR3;T;if2uY&vHr~zW$gVTPg^!YTcAz4 zZQ4I}2aR1}|A}WwD{I?~MAKA2`n07H&+JtWILiYukSdwv;*J0aI~eNY=zv14Wja*E zmZZ2#mtHc`f~(9X>cm`7yCaerfB&z1(ebeH3$w%N*AQri2Ez6a$(?rW-;vnAy~~i? zW7qC5Npiljf-m*ic(En%HD*-0FF9H>X^W`aTfSEBOOjZBu=qK`FYaDgR^~m~dZcO! z)eEMC9I~)X@9ck;s;Ui>o0$6ZmY+^CC9Q7h;Qwrc`pr&d*@`BtXvzJPS?8BL3SF-G zX+kng99*mhJr)qX(C<6H+P7d9U`Hh!R0OE@4ZD_Ta8m&eegM|MU#wpUEOPheETR(} zB~I3nNmR|95zRL+1Po2cFKI<iz_zMY1R2&b6wS2W9-T@@qG4Mm^upiTwx{?4mKc~4 zvmb>Il%dJ_0(c7cJEKyVGZHG%04)S%e5v4BpkjrBBf&?BXdDbZNrx7~eoQhP+R*1{ zC<0cf0?L9v?RErl6q8v92~yVsf5YgFB={4z6CDogLgGMK_P)icg#UI*&!O+pbTzbj zZfP>fh{m}1cfKf#{!eM9X^xTHEoV^l%*BV7d<3s=Gk@uLBdBmopn&7Y=LRc1_lYk{ zgZg(|Soh^woa7a}jVaY*oXDB1`_1WF2ZvR=*vY2p^9oHV93o{rx|N%OeBZvB9h(vj zbiMIa>XP>}>O6MBm|*K!SGY5pUgHh)S#tXh%}!9drAul8q{2{;-6NBVJ7)*-m(wZ& z*kf+(7Y~1D7<G*nBD=vg_QTSF)C9Zu;ll^1<g2GQ+AQb&=h`^Fnm@$ehn$Y|nsr7^ z4l}3TTh>zEB;3;qyt*uDpBb(L<djyzHro&Ftc=*y45AKf%WM#|YBw<>IjEV(b@05i zKvcCWpKUkgCdr=*+wgdf|B`l%cY@~+QJ6g~`~F!s4{mwDzr5vX*_+xX73_Kqb$P+L zZYSma`$wour+iFk?rW%0tK!+f9m_geeq@Ei&l$@J6KbduD0?YxzGOCMxm_;)Y5zbQ zcPHB__8HO-m~y2&&Wvgar<A#1?_@ezCS{inL#f7TDf7ceP>v{!pLnZJDkCBU(3V0| z_s$<<%~N3`Ube}tkfcTiy4X3%BG<00t>BFebx@3d(e}09y*80<oO~C2p=dZs4ugP2 zIOj3p0r{&^GVwKaJLbrU+IsY=QqYRM0wZ5ZWQ&ZkDepY$cq$%hj}4%h;*P1OnPZ(t zDssmGGw)dfC+IXbIg7Q8w(!_+h85IqKq6NN`FU=iWpSaBfG2ulGHe)4D_pJdb9~)H zTayCuxJw6N?+eD#3c<9(!di;bz$|soYHI{P23+uc)9yDx1=JKY^vLWFMW-pRgWkXn zxz0P+Ra7XpxxHS$NmD=80(}+|PL2?pktp|?*P)m<bHHgLpqtm>6y)yZp-!jIH1zcI zV>GHrHsj$Jie9JD!{#npP^h-ZoK2|a$7>rEXbi6&jeUkPemG#kF8<ht4vn`(6kMM{ zqG&n>MxNwU-!#$NwVMzPnlBD!LX@l~28jUOhi3uH{wtfveRHtgZ*B*|D{wIN2v{La z$H+BQaDjGd271~h4qVb_-hjN>!c_$<aAZuttHNkH^P^~$NhScMqTRv`Gj{?Bn5B`| zDf@7+(K$zBW2M2WSO)*}y|_VZIkmxM%HUiMSn*9-OX5YEh-H_B;Ci;-eY=egBo-Uv zuBZB~#%lqqQzdSry3`&g>@xF%rF6`(d~kay47I)%L)~~edyPg+!wmZ>mo{EGcMzt} z5#MQhPe;cF3*kd$F@j>?FUO<6R6se}pklfk<<ts23UV?>a2NSvUW$)CI(<9@{A71@ zs%O>Ent0>{d3FMO-3B9bQAc>AqXgMZh2JL6EjiCv>X=0>6k=xTeanv&9FeC^yz>O= z7GjqBC>16)<c`cFE8I5M%)sR7D>bJgLOjS@bwroVZ_axp{8K1t{)V5t&_`#W)*-wg zT-l_BtKN%`?}J%T#Trd2+A(T6<eMaqir;C_-$zVWTS{&X$pRhG=LdughG|ghy_!i& zQl{Jbx;{?InL28yw~JTg@mj+@1)dA-fSV?@lu3nj%Ko8Re!OBzIaPk)WObX)<OiGR z=h?$H{txuXxg<Xc35V93OUy;9ef!rc*It?hbc()LEM-FbjW;kOQkxtl#zVB=+E#dU z`N8K*QQITst0oa~bvWt5-iI!|9=zQOIRQg*^T{wH5||SJu8=k+XsAjXiS_2NvEd2% zkVwC??PFkQnkJ+na8|+^RRay(vSYE|$!8fl*lPCu3Q@z?1k=CjTTL|eo`)p516=oN z!z1JXUTiW2;xry`HkQUwP(#5EzvvelF7;$BsP$yJkJPfFOAOgqlY%w5hv&eQQJIG7 zSf@=%KxyIYt{L_rCy-JBg^LEP3!BMraOGjivf`B8yOkFnv~IBX*pNc=;ex}kr_g8^ zSz%>lUKX_XwwkC46nX=zp!@HrbCAP6=sfQSm{LGT*L!97svew*>qB~;bM+_%e^>@F z;M$#^U-m4ztf02fq5yQP7qyfOm{ignwEqRAaVs}zn&(xA4x9(9BmOov-ynwtf$3Zs z%WEYT*Gmk3T+hF{-0X64gT26A@rZGv-lYWH%co-QUC^5Eets%{lx?(IA(OrB>#2Sr z#rw{0o1l5vxQDhS#)+B+`36apyRkny7DUg~T4-B;5_~pxpC=)C<64>l_V`;EbiUes zXvvF{Ra)}$$r*X+l!n?(XZ^eSnUDeL@RHH;#A|m&aQZ*6mQ~^jFl9bX8E%{L<b>0f zMhk_J+cIxm?8X${M(wQ)CsRc+C!;iC-b9H`=eI!J#Iqg^wUQ6m(lljkU9DPe0f>w0 zd&b7RZed;sMAO}mO<$yAcaPhJ@cT0>JV<5ni%#fJ9;yI|>nE9H;uNoVFfuC2%D}3? zBsy9srTyCd1;9)tY;^qc^Ybi&BRuz8-r)jfuyIXuh#q|ot$2Qv^~H;cQu^n<6WcD4 zd|C`R?%Fk^ric~+?=EGgik5WZa*P6qj*Ci4c}k<(3ab@D7-jv&0PsiztYG*?!Je-8 zcxuCa)`YC`?o*?;Y?I!7JEd#iZ4>Z~rAowRcl+5F`)4=q>QN7`0?}V3?V8D+3;EQP zNg|Mvk)?dHhX`#pI86d2p9Lz225kp!-D7Pr;l6)uS^{B3dyu(r&WB^9ipK_#jON$7 z%ddz}LTc6<LSgo!7j(2Z0jrB2((Mcs?p8^+^{9<JcR}=&b0pol1J!NyRR{@ARjWOw zX%;P{koIsAJ!+_^3s`vP{D8iZa*Z-xWs+=hvAz7}?sEBS_be2S#xzTi&`BV}jjmC~ z@|BiLbllP457^w{F6@&Dz3dIF7$Y-|zlRQOxA?o1{8eNkU`MVXu3o<o*Zx>4`QE>A zoBKot{S}h%va+_03<qt`Z#vUnxWc2~^x=Q!&d{aDe>w(T;Z)OWwc$^E`{R)DC+pzI zhC^o>5A<D>z1<FJCG#w4<m?7{g$PbpvItzwF<2u8#6M4<)<QB(=2Z!rj9BMAQ{%0s zPgCp{sfgaIN#77x=2S1dloW>$Y1vhtHj_bCp3St4S^!mxY623IQ(zNaNRwuo72qKa z`!B!`500fHnu$2%=$H-smbS<Uk?08d>bq#_+n8?r9hc<akW-SrJ!~0Ag^giLc(|y; zwL=r^+7ijDTPsOsgaw$-k^`!@1r0oyc!#U}TlH-X)W+&$*oFlj3PloltI7T^YEQke z;PxsuD1Fhsi?&@Q*td4BWMC!in`h3{01+g@E*f5vh6%et+C!9>+@TqgNSRM37c6U6 zDKKNIGte?-0wMtr1pMEje6HE=@7!LcLY9c&F2QQse=izjwEN;l)PJW9KT8VOiy-!f z*oLM5g2AlcQI-)+^Y32_$~G4>44iEHJ_2AVy-8l}`o6zVYtib%{~EKN?^n(^wc40w z@5A85z<)q2zy23${+C4=5HrHiLv$Ho=!RL-63tR<V$(_1PoK)=zDfQ#UDISHf9Ul= zC4DO@`hg?fRTX;{rd~GWgf%0KZ78Yyp`ZO<gbpBgv2t~l8-FgU;Fnd~;9qw9le8q9 z{zdD*EfIgW`h;!&VI;!cf8Mz8Pv+5B)zPET4|F@$8(yw4%%iKn5re7h<9}*TK&p!C zoGfQB?&;Apm)sZCuMF#IzF5q+F8ah|ZuOi~(KM`Q;gIisqp4%+G^TTVk2kLsEPwv7 zmmI{--tbQ6I1Jp*CKIr*;PE>!>a3?rmD&CfflbsYicJKqxGVvA`7W}c9Ws_lLDtos zQQZX=>EyeU2n52F;MS?yTYh(gRE(jjwP|9Z)ps|O@{*PpshXa~z-;=I{3B;97R!!= zxhE|kIxU>6D4s!;@O%$0b*_v3*sN3aZ-EdpR|IIsJ(APU=jKN8o>#7B=T!!+Hwrh9 z+XiD;=AXRXuuhrO_pm6kA&u_X_jX$Ro`S=SZLK8lR0;~}qGbWpt(%8-Bi)YX`4>Pc z5CPg-{TN0RVcOvEShv!tc#$*U8Jrs#y@GIi(~p^$RkZbUTj-(T(V$6J*OgkAt2-&F zQugGq0#TfwEX*>Ef9wEC<q@s$I9O5jb6aLe`P8z_mos-2Z11$fhA5Y{ClzJE;id<r z&B!5IA-2ATPo5u8pIy!(!`IPPzE@z++TGu}QBv?3>#5{rBFrm4H(w;mKAxIA%hQIe z^vA<&w5FbMWrpBkN*XqPx%r?pW9Ka%wkmHoqeHOXM9ETYQfS59G)spVXt~L}Gw!XP z$ABQH%O|35kx9WR6C<8E@`;pb%aENZes>v9W@VHyRL3lID0R7KY{2*2^D2{hiIE(_ z*D6Ac&L^tQPjl6A5_7`C2qeKc2=KmDPd3a$`6KI~`8)F)$OvGQo}0R6&yRZ^a>vEg zHMMx9-1oTxuqvO_HE&={PoIO;L_6AK>yC%9|2%?}B3%B|Oir<NrhixMy##j7hgm7@ zY)kU@fJvvT31?Cv^XV=F^i(V==Otf(MqW3)JtucrX`3@C(oMzl#+<1AYMlQ#52SR{ z(ss-Y)S;cn#H_-%HxPsAC*T3%S6gFt&7(?2ad63-5heVrxfVlJ9UGoGk#wu-wL7$m z05Iu0C6`=55uU{^%st5AbsJSkYWd)1Ye^cY*xLAEn#~91E1m<kWs^Mv^119cEWHXA zJxqzwl0&z=7qSeeox4H&5D52xxdTuwn$iJ=2Re{!nWA}ipcJT}Hq-)^lEO+<K1WAC z)cWQ(Wl&g^VNXfw>r)Q>;%Ad7sUvO{$2>Mv0$}j@3Wi(h$8DV#JQE9$vb^9xy&z~X z?`vTUjGX^<;M`<vrGhz>buhN6QNP{<y1;QAv3k%7zy&-f(j|yc(*0Zt#2$bjDl^O9 zv?RiUEliYf{+G&9LF02kokHwOz<^2d3@o#G7X0|q{PHG5Ul<TtRjh}2U}tm90v`fj zlwV@ewvScndZcj;De7m@5Wl(2gOem9f=7meB}Z8mfRzI@Egn$YW#RLOM<^%KdWL(X zf4Z@&RDIm_Ud3N^y?)#lLfMa>WG*Mu!A~kUP(-tqdJ7ek06p2g*ECi=v1@PK?ER@) z?txJvvUvd<H{%tBxt;)ZC+&*Rp~PJbz<W2B=9vO&s+=a#2g5;k;Bg(<@=Ch%X?Bx2 z%d4QYCjh~j;GILHHia?7icPf>M5H7NmM=Q4>U|Wr)3OO=Bs4w{buL`GPKoOzYr1a7 z;bC>PDct2d#u~Xaooo$+`a4jYfylHlFhn(qA%^DQj=v2OEnxTp%pne1{(ovR9ZEAA zHM03MWX-Uy-uX@N?}ev({mZHKFJ%0p5jJY=v*NrtEdqc2G&K15(?H#Q=kKv3u5(My zM4mQE_C>kX&p9-NR5|Y};8Bk~H+`@F@smHexbOTje|<?>zz=I>yQKCb-w4)1Q%Km_ zF?uvkQY*}t+PP~d&e$VECsra?>2l5_mwq~p-ZWoZy|KCzLBh>GyH|s+tec_B5vBt= zQve^V_X;wdoq+T%B*2XP<@|P5D7}fb_23K@5N4$65TJZW5aq><N<)BPOD+{=1k#aS ze6`5`qKNfbrZ~I<S)TZi62JD<RNP3ItUI5xwI-&Q>FNCTQl0PJhf5QtpYISRkL&+` zq{uSwiirBLCLFbm!7N|@!BsB*Dc!C)kfG-uV){r2w{`{Ja60UL<r(`m=eNt}|Ax}r z5WDxyhrC$Q-ZnXgKDzsNnBORq{{qAR%c6{tFw)4<q;~8;uULNuBY0KwZV@M(Z}rL5 z*jJBwPk&Z&nhew7f2YzVwEqG}yQ}ibtgGroA)$KA>2v(%);$<<Ji4E8>NY?_(U|Q3 zJdtoQY}0ZGbScNKn1RJ5<U$md^s6$6nZ;7H#K3YIF0~uU3-)aEZ~jg<1)=op75`R% zb<-IITlRGZ`wNhCiGOlMNH7i~`3u_S0K$1Ei}jys_-}(|A=|i=x}~N+P+S?h@gKFT zNvAw<u3rCLC;o?b^<ecQDdK-r)gJ=}`&}2n+l2=to;}+4TARUNd;R~;1<}2W|B|$S zA>)@-iR6)HUC#ZD?ESWnPHS|i9pX$9Z(cCI@J_L#=XM5D5ivjo=f>v-I>H=EhCWD` z(xQGswLn?=*p_oY^8eirzivwS8#Z5IJ@<b1Ax&!Y&(|;}rt`leMA2Ws_rEO4AVlxA zBYUnL3vSqU_UUP&^MAVCv4^lW`8We<`tbk%`#%{6=&nD!ppe^UpOo024`wl_{`Z~u zd!u{*Q<}_gJMk%U6W<o~Zt?$OY5A*{8s^(?lT=QQ;>O-o{p5OP^pqY$*rffA$JPF7 zy?<2x7hzM(()5Tk<R|RjUDOM<yP7ZJ;}b^~E;WjOoUduATIV=)u0`qgNIKki2lG-H z^r*4;KBVYR^hc^9`w7df|AU<UorJXm>Q@K<!e$0E&3`PH`;X=Q9|>s$g<5X-7j|zz zi6qCuzhX04AdFc|{O08TFP0MCy1f#Q`3?!~v1ahrQOs|IVyZ<v`Xj{u)ieg7(xPH` zMA$-j-<)vaKl<aX{4wU48$4|mOUtU58oS$SXf$9{`24wD&9h_25A_D6CnYsLx^kNB zV9;;3DJX4s8$ZvOBw>CtiliLej9X^j9$PZ05!73vgagukjH6WN`?Ga^&6e_`Y|dlR z;`RR29VJ4MobCQCs;0&OwKP2;U1uA)PxdEJuw%KcxBD_7D-N@YrKFiZ+{~NXZ+^DU zBD1r=Znf4|^~G~st`B|%GH;Lds48{#<@><yeIN%Y^tRoOrvAb?B;Q4`K00}0mQ?2z zTK!dm^@d^{-;(qVApOGTo7yTa+gyuVT$pX|<&V#VBy==+w|r<(=o}D*Ewn@e0c(d9 zscu@fKR1Q6;?dH^2q6`c6t#v;FMaG#3JTksob{FvX=>)Ca`S8&QNGmwrs@|>uC{MW z57)>aSx3Kj@M0c9a_qzq8~zwzx9emc4QB9U2wZ}t)dqF}%g$dP7<jJm_!#*A6rTx_ z=vDn15U6b6`m$P0wL<_(I;o#Vr@TXVV$XmCNv_HkU{QHjXnSog6G))@^UgGs532wg zru>X9288Bq>5WEQCw|gS7GJLr7hR31vap3c9>CSn?m3H^nze}SWb@kB5ay0N-Z6xQ z5xRFT`cKf|%d%@O6ZCWSLM2e2UfIN@>;2!lpK-Mjc2LmGd$jp8-Esv2v(hU%mU{lZ zh`k=$gk|S5Se_4j$xEiYD(RVwtreMV5md5P0~pgv5D@m4VA^gCdtBY?Dn)6xM5*tF zfrRg0X^KhTww9`Se`6Cxc~a5@tb5bBQKd3iR9=jD)xTvdy~$?&N2U?_u}h$@wLY*s zauPA*@*dvwt*2nIa>kJ$klR|GYj>$9hq1ah^QoYjQc5+i%@3)Zu*ep5_1K{Kmf`Oi z%n#B47My-2R0K3o&F_mNhW3c;@*Ti=y$kV6WinpM>kDi>S!2id47N3!YS!ZYY}~?T zL4UV@(|Nl8k_tx#YePvBQGnP1{8)W>Dxsv`IFqRi>iT5U4gfp&KAfN$rY5-!b<`Q~ zj5tvSwl5(JMj&fO6rp)e6|hUAZJZ9vVM6D|!jeQyKb_5;(%1@U@jh@X*X&+Wz^?%J z==6nUL_THTl1ZN^Lq?xT-+9QKy@Nct>^yZg-m}o}!4t0kMcI2mHPx+Mqkshw5fMc& z6a^I#kS4vURHZ3MlPXnulTL`J2qHy5nl$Osdv78_=%EJ^AoNfIp@#&L+|BFzo^$Rv z-@X6;jlmc&l1;Mr+UuFmob!41S|vwQZ6oPlA-w71t2~vp1JW$lsxW=ZluJyNRnk&E zbVutG-B8>-k3guF&H$}%#kwvb_T=n^X9lcK_3-Ffi*S3Fd__mws@Nt)%3`0AN=*M1 zo9BPGgyK`)dERhH)a5HsWAs3>k<jcD(V}mi*$tOV_;6>@76#t_7a(WmIf`1n$L~xN z_LGiSL`t5+zXly@7HvJZ_JVWJM43?kDDA9mI4HHXR4pa+rlDCj?V3|@Vg8#5dwq{` zMbcLHo<MdbK0;qt?s)I?SoIBi!o7RD_Uhj2vOd8r>Z9GhzTc84-W-FYpJd5Vr}8Fb zZ9iPXe5H3y+GBm*kv_NErD92XnNV<?x?|i~(6>g2Z2AoXp5iXTpi*m3oAeFi1rDV) z$SXZC5i7KGccGN+W63V<)QGM<Y=DY>U|hl`+?%CPxNu#1$Hq_Q5i}|q6=sXUn5{z4 zjtqE0Uhg&EL1H9qn3UErU%4Y&np*imH)(92%~wP=?FNV(XS63~wsBOCAd8sj63u!y zF<92U0<y!53AuC`VnT*j{Q2w|d-B^zez}ix>2Cs74|x;pL`j8H&g8dgbA<!xwhb@) z4FhEMg*1ITiUO3xNe+ZRSAr{(-!B;_`w2KCPr>CDWxfH+Ktv2ggEiCpml~Oa27>Wv zlq|<`OP)*mzQtBWJw+v@!&ZK*{{+vyl__I+eTkncSuwqE4h~A#Je6VN5qsa5)H~uZ zO!2C1;elm~t;}cIz5`+2in_O(lVe0O_qu-d)pD`sG&kpobpJGJ_Z3exr+1yq@C%2% zf%{49d&b}0v*TKvmq1#xN7ykrT})IWW1wXS*9-ZqZQhdgG~WZPf75gPFTN-~(^NNQ zHUWc<BQJ@D`{VhxjB{`i@J3hNJeAfnJOmSUA<+5u!fRdX8I=bY^U#NP&5nXSoCEa1 zFKqAlm|Cvy#qud{5QA%=_^V~@L&bF$cMA8ac^ATHW!iekT)bd`1zm56#{=50&0sy) z#i4*k5s{npPVw%>;<}RIPHU*U{vAW9uBk~aUe;4fRNjNDMn^>ipVIY<DnGE*eT3FX zlnEJCE^jNn&xdwK4L6@cxOhu6n3Plw!{bP$Fq8!ZNAB8OkS9R3^j4c>@pUgJ&2|Sm z$!teiT&-T#$MSP4UiAZ-X8N7v)XK=K1^cb;GluGK*AXt#k(cmg=>tNQwiS}DdZuwI z)q!$JMrOeAb(&hWIyg3#^s6HO<iaM%PxJ}``@Aluw#8_H9eA#6;wO!lHAKt!ii4>H z#rW5#AE&%zDA!+F_%)PPAgPThBj>JM*TT5wM3$bhU}|x`709#&E^4_5by@L?6Jg#S zN~CjATcBMjT=3HE93YvPHriEbHqg&3)#<Z6I%wpo!udq5e=S_4ie9!y^}PALk71>C zqm8JPqRwiCDaj7f`Q085M!Dg66fC#L3@NLXI)*l~-IA(GdVIB2i2ND6o)_-Ys~OWD zy)Jq-!oZy~b<cDnqoUQc6jQqGhLCl-EbAMfw!3ra)lq1D{0V7HOmy!GOMO6Doet%Y zjZEJ5*I%n6ObZPk9i2{<mYuF$+VNpUtTVbE0Xs~{5t6@lC9a-*VLTd1<YiYVk3B1U z51Z6o7?9Yivh|crt4^V3>6E?+KK`mtq!7@6i>>}nFKMQfK3JYVPuEP;Bf3FJl>3mL z1s<>AyXYSa-+Q9LFifkF%F^j=Je&8Al=Mw#0~H%`Z^55`{=LgyGwYNNn-x6HtTzoi z0Ds>@Iuu?SWs|mhaVOpT9%{gw5K@~ID|L4CY(pLtY6~x}iPa%#Wh_v8^kHV~qo&sy zu(w}E7zLX3mRhZjm(*r&SMa8$jBoGN_j#L4lEweN$%l>K*O^UVQx5Et8&ASLzqIgL z=c$_%Ud9fLzr#|xJTi?!mu*VRs%U3a%BN7QdSOb-nZ~7Ma7=r??`m(jFh$+c+!O+# zt3qX^P&v;w^%kyDrg^4k$y7ma$lNJBZe0^mEA(Yl$gg7-xQ2fapT+79<*}2kCfySV z7>G3eV$%D(UUM*2$SqQ({#-<kw_JO@|5Dr>t&DQP{_bwXK${*Z-MvD%*?Drt(S<w_ zF_8xiD0ixHNv@&do_Djr6mMjXeD2+JPc^rOVAiZ{%E?!wR>aN=QvY!Di%Iu*G-FoL zRLESmNk8l(UOtYg&APX#3y#<<)-Jid%fj1S*Q!v$SajTqG(*$WBGLn&7!#i#zyr%2 zAjfBY^I=?OwPbr2>Oe(b-6i^;C8h<0G;>jMR+?*g1N9Qqkwg7vh)cD+R5t5aZwawm z0eZ2ze$#uhn;KGboZh=GCqJU*1zVDOR72VYD@oOM4VV==v-0{6kvz%mvY67{*0m}3 z@0H0oxBl3czDg50IdZSn5SW>_`S2LFbUi@5DR_T=c3<#-+$eNRI-#hW!H&e*FzdIq zdnNctTdCj@c*Ay0SZbQNJ{-p<(yO%~U)UT?X_C-###r*`S<oSTGqB&NEXzTQeC|je zZ--H_yc*cavv7{>H@{5UMep3E`@|>_w?1$j{X!aq+pEBH%lnpIc}$V>_1Gb=xt+Te z?iCHc$aN*6kyzpU=1?7J{kqRRWQTAwnA#BR>u#~cV*%>vw$#`76(!i%ca8+^&ys82 z@(?xYVIQd-AJA6A8JT!Yz{47Pq-EuASaZqMkUYE|Vb*uP^D1?tB>-X%k@1l_?B4wn zkEvPXcHBzZH_0YPS=VHvR)wke9HSnOSc~a_tq*YZp=rg1JKDs%wG*rDy>l}(dL)!} z&%w(VtQqSc9!wu`U|&J3GK;N+sb^w{yw2N(2Qc6<-pBh14N+9-FWf$q=o~}f<Wr5L zi>R-wGRf`OLrAgF9hmGf#D*Mc6!4LZqu<AQl=*EwL-VkxtSqfewiSSZ)~nzFk;2L( zXYmeu(lys3{oU39Z=0nz2iQ8LR&N*{Wdv=8OTWhjmy^vM<^Ms=f0EpZp4h_8G58Nn z9_>Z91u~M04w@^W!zeoja~Jgk-w$%0F8S>!J7c#`7`5F1F!>yoVwnC&Jgr;D%Tuyn z1-G%Ck11`n{MEvBFu5vM%LlV}*(%=g_TL}-NMOGdG7g%q2vI*ejh8KS(qCEI=@Rxd z%RY!GB1=q^#xJd=+q<O6HzGh44v?8%MZzHpRi1GGaccTVBzjYi%I&pZ#Bh~-2j?Su znc(}iN>W7_Eo6q4v<$h3?2&rxn~=PjF$zwmL?pL3d;$j&!Z*vu0;)A)K4Q5gSQK7d zBb8(nx$yol(oZA2>pKW<KK!Vgw3v^@K2O>wx^o}knm3KnkYa9h%$t-KhIKISy|6yN z`-;0cvOlKu14g<7-DJ%&S}1%AS~MD`w$YQ5l>Ja#eCZ-I%7H}rCJ$?&BP+ao6p%Aq z4)d+jM3{lKN;j?H_^->+&B&;OS>o0k_1P31v2-_c1@cCpJ*y3|N+h1EcXg@^5CLOC zwB*$d)s0e;3GQ1g`V(>Am~%tFw{ON|X}73{bgU)aBFz1;1+(xl@Qn&+;8?1m@VEe2 zENR*^sE4#BPU$4{`RYp2a}(4v{ZtM;G)VYJrzCR8&jgm^mp<&lkxGp2M#4n~J50RL zOwpSKQ`0`)!)bfdQiudOgl`yd*E6>1LvPt<T5wx%Q&60*{tt<J>3=I{|EJ^sNL-}E z1!W!8>rZa-+=|HlawSGtTh;kXviI&Lt!lSnDI3y9ByDUhB>UXuOo}fAya(`9;J<!+ zp0q&7BqSCZ#X)9k<kpS!Pnes;Z_Ty4w4Uh20vw{nmZ&dTaH0Jus0lWIuoX|LZ>)(N zbK!=RsOZ!A%LN`!jE_A9n&2HFguPWb$_3RzJl<LAg%C0iu2q-r@Z1v^SYk#;YZ8r1 z5U_HD3qIB>)yVFp*;x52*wcYjl7JNc#IS&i?Q4<w>%U(3A}MSkkA6Ywx{?GZIbPdy z7`QE8%ZQ*_0v%u$&4hOjOIH?QLW_?f)fb{Ys;}Y<7onoFaxGFH<tTbAWrZ-4-Q*&Y z7Tz0sVa0dR6jI241CFTSBla0|qx>jVj^ffh+Xp=~@Lcd1U6XY=RWn@TPv1ks7^qjn zP2{Kj@Fg>+-*y+s+Me?yk17t6euESzjyyBlBOXJ;86J9q)3OQNmoK5B$O&=7@1s!n z+PA;Q(npCIii%Y!gXfWVT?DX(JJSSq<28o{vHcRC54|fJ_SCRtTh-L9_=A?lc-(I4 zF>GQxD&V7U8qTxj_x<L$WAaDRE|W27uksbFJvUP04$*rR*?w)&VU;Tt_Jwdue!v8O zN(1eZ9c~;&>}ugd3T&VdISD(p9P`hS4|`C7VjLdwJm60QeDya#jx88;${y{6t-%d0 zjMo|Tb95Db?16198{z`)aat17&L(Cu$+QRMx%XossDymV^L6uVi<5jL*xw|4@P1$y zc+krXM}8{3?t^yevweE7zGr9>GgG+^d04Z0>OQ8BCuz>b4WbZ#+!`wnM#(*d9vP(n z96%-RaPuEiERJ-O?>6g4%fk)`$ra^IpZ7YD*U*HS)w6q_kTeY)4a{Do&abVo^+m0e zo}@i(QVJa3PV&MSsmU8X0_Vu{6TJ^Mma0dchQ^|aU>TJz%O$lBajin&(55`krEQ;O zb-$Thc6MD-yWoYBpJ$dvnB-#_xmH_a*@L$%maQB$;O$C{md~`!S0U}k?Y6eIBWuUA z<1{N5p&!swKlk__o38IVnAytr?R+m)mQTs5EWAOEn}&M{1_Vq*4uU2ivd}!$)@PlY zNL#cArglL9`kG@Rdix0-^bK(;@EY7ddIwE#Hq8SE)nH#P9|Ef!D5k9^?7}d$yP_+h zt7No6HzbFw@3pm;lc&W7;@RLlDz_9IVD0rXoQ<96puIpwzudtF1kTo*-4=kI;Tzb4 zxEjCh?XLG7C<ERR80=n^7V!CaZG3LPWB=G}^=z%AJejTVS>IHt=yap;hEel^Z@|nX z6>~d!|96B$>L_gbFx{47@u;=8r(E5CQU0%S5xjtAHk)-2Y59r%C|4D;a7dL7LQZvF zJM>$;Uf6v|W%fSx7<%pS*jR_OKpP-a!b7$Q2#V_HL5sKi{AW@-->;5)v;L9w738wK z<8Pd)!a^L1!ONkzXrh^`WEL0<>!UB5Jme}784*)BH4;=)|I1;k$jT9K32D1iYp=$e zoU-Hs;k4c%$lPD(Ni$DwTQj{Y;@7tWJ6Q2NpWc@1h~yP(B%r*iHj`zNKEf}xmxj=_ zIEb5&1sq2#K}P08EyMwS&>7qeNAG<5P>7|CR_Q^lN0y)c2LJ8%<_klesTXQdm<J*+ zt?$(7zAu`3%;vIadB9PbPI}^pktehyMgb4&<NSPZ;r{h^CZIQug01WQ?vwJ58;PmM zBiBYdIEP9B=>XZItDhD+zB0bM*tV@=3ny>*^A1Oys*Qob*&UC(WhjZVV3p}+3_`i{ z#}`uq566CLPz}cK37hMpN^9IPZRvs<hN5UZWx&7Rf3h28-G!w+`lb>Gi53!Gg(8bf zVQ!^)Np_x2HrEnj<y!Bzj*+IpMJ4^~J1HfBX|FK<+_AE6MxIrz$-G<QhdN$vMeSJ3 z@_6xq??5p6JX^E;%)4kv6qvatV=3jj0UqtW(NPnP@amICEtR42w{VJE!0w^=m&n*H zem8-5c~s@LZ0ni~LvLSTO?Yw$7ZEkNTuh!^Zj`GTnN)Tlha)-NNQZu9l<vi?&sSV% zBs+h*=kgtfZWiY`_Zu2Xvu@<MEK5=HKpxW9@tec*_$S0aAK&#K0F|RklXZqS@}>!m z4@-YlFQN9+)fTam94nj0<KJ7m>tZ2t^it{0Z6d2z(dW&K$Y%E->rQXVES)gz3UZ5X z*<>A3RfKx%;U(xfnF@LIWf|wpsG(Blo8(_|vN-g#%ubUS6e5jZfEMm~9OG6<Q~yd= z{<A*4;Yr3FkeRUQqcvAHA9RhpApCRFH7>Gf8fx~?Zy7oimGs`S)g-sFBE#pRDPheq zWT)wS9Q`2lE+gCqUrv?^<Ver$9huBBrF8smN8jO$7QHT6TA6<>O&<yitTmRtf|U0n zIY%aJG#;R1#H{<=b5KLX)?Hl-oj@;q9x2PT9;bUjOI+*jYJfB^UI(_SvUmI-Kz_nz zgQJQ(h^x6L4|R`B!089Vgkdfnnxy#2^d0~WbV!n9c%W6}t3q@ldS`PyWU3qGbVc>E z*a61rrvcfBJpGguO#Bi!;BkN9GUAkP$`JXB)B6XLHCj^}*sB4Lf%Yb=<Fp;zp#b`D zk~)#pW+6&KcDHu<lBN%_y8jCKzcKL-G72``|NV?9A&~ww2kGAUfY3t~<N5T~#VNI( zt(i+GcQpfYVzq}bWWM|^DrX1X5czZ+Q_IvK@ij7>TW&ta;Ng6H)5U@3MMuwx*DbPU z5pH{tsXN|2pJ*`_IG)(n_02l79<_kDsA#W&Yd2=5gwT;=8ckxC9L`y%Tk{J;ld%L* ze7a=9E}OiXX{1s^cHk$;9a#BuY<Y=c30%2eB7PmhK(8sk6eukv`&1?Yi$sw!_%UxN zwi|xO_-a0y0YO)78%YYJNE+k0BTeGGL66#6f(H-xs;L^;);Mq;EeuW;+W?PNw${#J zdFt7xoK3VEkKAY2OhJ9PO8bz&kDRre{v58$_49aPN$<B}Us=Zl2Jf?``|*2iNFDJK zq2#RH3Jw1m{RDPm)X`JJ`qG!U;-0nYO!M2d;dC3r*^ZJ?!A+OZaT{qhNvva~FfJ{a zt{?A8n=V;idXvbA_lQJKS$zv+_u8J-tW6G4Oht>|yKjgt1o3)Fjrdwi?;T3?f^!2` z$<qPBwH3VW(KErCBuPlt?DS7?od4raV5PjXrK^c>pUp1|W5pg(2HmX=rQte79C{nc z#{y&MM$f0RE^8N5h^<>PpT`>2xye}-yXr+dQuK?8CKX~+_C+?SNjtXciR-8RQUywR zCz_4?Ba={U4endh3VRwQhH$ssz_}HpMppkQ-_K21B|H7T*J&)zboo7M&70Y4+>4%( z9@+H9ekd$|3oM*a#OT-U;iupF^+>fOWf!xn7xKzSgiP`M9pOf7)uIUn#c7*=6WFw$ z{}Uno-@XPASVV-=n&6d>ug<=TzBv5I;KL*4?9Xp5m1ugR)<*K#0;w{CanYQQ16iR^ zuXp|1cmL|J-9w?#=<S6C%Z5=>>kaHenjMOZs8Vn==_od`<SN06nwe=ChwL_caKyol z3MO1?k=UI&PeN4uHfqC5K3+TB6IIF#StIfOhS;>CAY+tiXGS9krbj=n1(bsKySJ;; zohl!!i$o>mjKPXMcfM@jY<}6rfoIN<)Q0MQ8zUJ)mJi|0OON^d$gu6b&%Zg3x&s17 z<st<F&{Nv*0_?sax-U&BiK9dX^>sa|R$V?p%0`d6!g#Hr^b)z{nOEguYilc{l}lSL zWdv!N8(scgv#fR4B9#E$mMta}O>CJe`^g^oYUVxxuT|Vfi)>9U$cuc2)TEM9`f@I{ z--8y2Pv*shAktRzraG&I1GD^60KC#C{4N`}g;7LuMKzHZYi!H!kUj}7_^L_6yifI{ z3HN!WI>pu`V9#c?aH3QyiMAXu<vmSSfws8DDw)ql9q9&_k&X_ssU*|EqgaeX<wk#+ z3a}qhDLe!N6VV`~nBD<*%Mh1Qj}jVmkJaLyx17?OJir~WJFGqF48qY2XKK0)NN&Rx zR$}Xz><*6keaF!!O<krV&C9+b)2+e@$H+yAF4>;4Ku~`md=I6Q@Y25}5@mT&)J7Oo zY09;Lf{N{UlUBsZ!v^OT?tqC5!s%IFzacU<$b?}KINih{e*A}t2gb3_E*`BWWESrM zF-{PQGR|vp)r_pPW$UijA7^Q|Haei`n=YC}^-Ap~Eqtnu*tY5X2JCsbfyEghg^T$b z34Mv>uEocZf$FcCVm7jSjd9v(r9%rb`%a@I7Kf~&q$sR1upef^Ze8`DGBet!k~|82 z9yp2KCyOo0oKEch0b?!ftLS-fRIR*(Br~=NWo={~M(qSdkdEcKNxsJz;?3Sqp363e zx6<(G;i5;d=2B8$uOy{~Y(rl7BZ2f|xkkNBP`^mIEK&rui^!5OTW8wF?%uu1xbt;A zxFwPl+Y(M_@*+#(I^+m+B$bCOI;}q|YoM(-Nl6vod!%CsXX$go0Xx|+t<>`#3GbN4 z>L=ap&)*E`nYbhnxg_aTNX+Yz#XXO2GDCYL%B5KHp!pn@r9RmXJWuPPCKuoSIGW0* zv)re=&9Q|4=KW|HxsxXBRkhKi-(xwd@8*~bFNH5mBHO+ZF-8rgQ&8|0-|BY1Eq+Cu z8I9XjhOZs{I0e2h+?%+ti>Q>r)p*gXEu8TS>@@qY0`LF38~(o<{Qk$|htH5G`KN7W zV}(15fb#vL|3Dnq|1G-wPsam@Gd}d`{j*DFp3#;w(3W2Um0vpf!>v9j=o8Sv`=(uT z>zQq5X`vR3@jw3mEo4q2(gTG{@UvfSP9{4<9vQ*+S)to0xvOsKk%rCjN#BZJj#uzS ztrxvw4g`O!t6emRaS>K8iStd*KURYs`_bbPFn(#bt4a6ao;w>X++_C+H^?N~pR{k1 zj>g|6?=6zSF-{vto-Izu5or_-f`_CNqSo<aB4132$h>%6!i?L-zZatUk8}I~`7>aa zx?C{${tdpcbqb0b6i*Z%y#NMa8vMox{R)j2P&7JAy!<}b?3{Q$<tH~qLnl?k=X|%N z&vq9*4V@ILdZ96I)v!SsQmxm`uAxzqyPongtsHsy%sEcS0plK&mNJca{-Ux+llQ&n zy~}I2Vv4G7H^kk(eHH9t$~su1YMdPDb6c(9i2C8>8#I@)A2Qs6q<i?NP94d>z_a6| zCEraC>;mHG*g207M7<*GcjfI&mI{8{`(R$;A}1#&yaz{GLuR9&-{&%{#^I;c<6o)S zG*+Ix^X-?0qGA{#fn!o`L+ZRZ0|U=Xt~Rmhw~d?j0vPxDk{TKY2E(5=4IeE7zJu}6 z8yIk)x2b2T)Rdy4qK!h!BeA4+A;^T5y_|3uicG@Mf`Q|sQ_f#^HX{rtOtgTd>E)Pb z2Q!#~U*Gdr)y_*WFx2OsXL61`gUnD6I>2h3H5NNB$H2g{ojNU&9;U887>#^Sf0iPI zn&Z|2jzpYXfBh&dH1ua*{IRXhtiSWc*&gQlB?9p`qcp9*F9SnT?ZBQU(I#fQqPahN z7FNFmatyiHaUQ&i&$<~tCm|8}sP3j((l|RSw9U=$b#rbiBSRATJi9agtdaY9k&qz@ zX0nJhx!h&rNi{0Aaev4?_L$i)_Nk&`Nsh0O`{(L<*Lvfs4hpI&(5qz`<-T)|^efD` z<VnjV^_y{_q0jobYM)@FE~$~`j0OsZvgkfUxMlsInM-I?cZgCrQuU_VFhnFq1`r;Y z^2)9*sYpL4St^SL-%6I7p8Q5=WG02Y4eb&Y6Hs>MdI>t!E#D`nN!5Mn)8(BY7MgkY z`g6+aTfw1`Bo*+QaHGA7a`3|<e#y(b;v$u926EtUTm36;2pT~>s3;<1&x<YdCaO-D z62YxLMmSd;veo~C%*nZnYGSTGgNV)gx@g{ct3vduOtJJid<^tr%h&MCl4aT76ahEA zs+J9H?gZS-Wa0WTI{zifxU@Q&BFXd4>j`yYo!6x2jmi6z*F_MbpH7?TvkU`sMh`ob zH9|0aPJB3CbDl&fUX7Tt@U3R~+;Q$S6-8onarhL%-e68GPEb;$8`1{mz5d#KO}KAS zo^11B_Ey_anPk*@DyzP27?$#9!BW1<Jl7S^a~iEsPEMs#LJ%e>&bc6@nU>vogr&a2 zLVDL>?;*d_EfwdMXJ6Aq?%R7mg0nDgQOe!vy}PS^%|sp8gJoxx(Q!bVJ0upIczWyA zWg0@#n~gB%l22V!UtpS3Iy@^?RysP2S~BIa27K{w?COLrQcP-qwkmH^Xv6PYD9vnE zpvT%pBOfF8Fj)B3UWrWam0|Ug1W!L94Y(SsdNrsg5$SltanZ+AQ_+gy-b9zvB3pDN zuB6Lk^sK(QqQar}=ew?B!x;K^47aa@&4>4mS*x>nEj_=(k`|#U9rRE-awN?Y;bxCF z5kPv=R!K{2Od>qDwI^;|f4cAX^@$aNc!j~R&e%U4wedoJ>*tDK8E)ly%YJIJugm>B zZ-uZ>)%*D6?*9tmGiLleoSee&E6$vp6|;ANh_q^}UpsRfKac}Nj4|)^`cI)<?{v08 z>S%NF^LZ9_NybRu&!M5&JUy-s_VyM>2OBIdB6jxn+CM*qs+s~pp(V5Z`>QoO<aoOX zeTTM2A;bcM;z$(yrj|F$$Pmp?DeT^R)fE51%pLkCKD|qh-Rq5CP60sjQ`F#@aHV*n z#9LlnZwxL&A><A*Rm(gqv}^u(RpD(FF%i%DpKk6hIhEfNn@^jh<|hbTtH+P(GZ?<G zaeDNG5bCsuT~!A~I@)|=ccchP6*A|()V})ax)D0Ds^0YGZq9Mso`h$9#N*PKNwq-~ z)W%ZBzH->%C+7Bwyx697-<e}gV78AI1tV3z^zQ*BN(CWSZjTR46D(XcB7!eFbsI(Y zCF2{*4a7?jl~o5-v*EhX@HZ8r77XWnbEjM9pKD6+KeKdV8*O;@^HW!*Ue!oU(A!VF z`>8{yRj%4-4rb7twJn1$fsbyEzbcA%jn;`%>bfISv^n*C{Wb21&yw1%Rs{ZGS+AL~ zM@D=b*NvivIw<-{RRqR0&pZnWXmyFUIr8Oo)xiY&!SFXWdy{c;5fV$dr(xAOaO{8w z`3*yKy~sMO-el~2XR6Iwt$?QszsDkQ`XJe~IJ+HfeaYnXUQwya(9^h~Ci_#Gqtv&^ z?>Aj`XRD61t<S!e^N^3!NjPR+@6<(VWa+3-&}X2>cZLt`o>Y2UG&hy#O0*2-mdh@w zeoRH7`gew+O-itF*J&y!q1zE-^amD-g?R{RV@0chzNdBloogM7=bX^lP>&f85$e^< zaJClKh&(F6<0*rTpEcDF4$ZhL`c;HMJv1(A4!^xYbbCX#k8kKB7uJ_Ni<6#4+yITo zj0c=GBf4K_?we`d!zu}AVefG}hNt3!H}k6T{OZr@KB*4f^Ql|%>)f!n0%d4Oy7x+d z=rKqOoI$@DdvKK4`q-3L@+v}Rr`)r>j=|hMJF$E%JBCffiQ}!iUgAnxTIiMH%mCu} zkJOeSd5!?Vw<EE}io(BEpO8{L7MFL+!YECpUNiL6yWWasd7Inu#JoeDAe#exsK=ux zxr^tAV(0iJhDN=`{b>l)+`K7Dkz)zVdw%yg-HWeS*NKXXmV4NGc9wIeP0&0<@YfDm z*5hAe^MC6$M%^$PX8Mw?r>3p-VA0Z}UFaJw@q>0&+C-Y=n`-A9Yh115(7eZc;ZHX4 zQf8DTaRMXrx0W1Bw2lU-k77ou@S`~0%B<@5h#G6VtVf?!w<LPAUPkb6b`-AsIsugt zaOIPMVBR1p)&;S5Byim@8h(A;B3t=E#tPH9P0PIsItL+bTDhGCOQSkgh?B~WVZ+C2 zW|)TFfX-WoC1j3$rtJH=;oPgBm%Mg~%8Wq=OnwNdu0wg1^|O1KA`Cn>CCxCbR{@95 z2|u6&h{9JFux;tg$Xx){NB;|8l*HX*8Dg8C-%g!T<m8Nr_*MKG|JwWI>-2T!(9k{w zrK69Rfn=YgU3Y>rS*`#?oXLBQtp|qBI$$Q<0ptw4`#_1sSwlm^5!E7VsWS|$Fm)y+ zG;}d%b}k;K7VFUraj!6!esi~zL=2+Is(<qr6A<F%sx_XA(>lEEP2uXZ^+-4n-BX(6 z9CnYBlNMGaa5rhwp(=*;X9s{WI|Nyf5hgUW>sS{6nUTC(d4%R0;g~R|0Z$c0#pPB* zTJZ!RYV6g>(q?}$K6Pcn_rg<fU>Dc&B=8B{ajvX+>ApyUt7C&TTo##<GpmjL(|`+l ztOG#68Bo2(?ePd+OR2%^{;@}b7PsY<q@WRp!#%mVmyw9hp3_`<%Bj&_%6V*D({@gt z=<p>2Bb9@S^3A@$Gyc`yKSEV|ib9a;`hupF{?0*C;erobG(IX6@z;Cocje}`4BlbQ zv)+TAQ>c`eeSKD<K2y%OB`>CA8SHqQxVs%*(Qg}{QyuqROzmz5Lh6Tg`=WKf>APy0 zU!AJ0HJ5Cj&E4UQ5#}9<2{ve!NR2k)tMLdMKl@q}S>~bWphdeuxfQ%d(^?f(+$Psx z@Ofx6sfd9hV87SSTyR4bp$Tt6zE0e#kCKZW9e=R&p%yQUwBKC#Zm4mW$Z7K@V6)@s zrnMY)jS|&shH68eRo#c;t}z%I1DT>e%`G|7!5`MIuu@;$w20+$d=W)U|5??Qqu*o| zdT&xF%SAL2)?QG=%}u)qMC_$4-Cg54dI;en(k@GbA-Y2oH$_nv>L}Y;FkUgV+I=fl z3@&GiXtH*+A2v`^pe#vroSC;8kB!^Q)(E)+>7<eZ3<?9<e(HVZpitGPLoG%X4Sw90 z)HEbD_m9>Rqu)O$MZ6u^e5K^dVZ_z4c(<!cB1x|QTq5<+N@fuSSFP1|uaoe69}co< zvG=O-dWNjIkT)%^C+Cy0G$ch;?)(m1D5}lK(4F~DAl|=^`xS@5P1Wps&b!>tf^Ji0 z=W^p1tZ0Xxi4LVbGF}~l6vyccxr5+Q<8*d&aXf=FkF|`bS;)*U#b+r_$Cnp)ag<aE zj3P!dRoE02wZraDDPO_JMz`2!YI+{*25CsF7D{#|(2veZEHF7#+43Z>SJFsgi5kPO zhyJvhN4KoEhc>GOS9yRrI^z$dcuo=BDz4fywT7!NxKx1`6EiP}JLv&Ip`W$#OCvcB zNVQ}48eC8Pc`fg5yOU$(Ff+CVG&`D~3M{Ja@e3=BRgc7^`B<Qhy$t}?uNwUkIk8?F z1M}TbXX`)SkifSDI;4eqE)>WnA4@|;Tp#W%b(7@8Z2hau3wD9%GZ>OPFX&$Cgp!-6 z_uyV1za3R-lyKbS!hvrMbTGxZI}Pjk$B(Ct@>aW_Ne06FMb(_xWn=FdmW6Gow|hGs zC53a{&7TWVQ5sK7_v6JoepN|4xg8Nyzj+eD`oO_~)r)N2zbWb#A}1c0rKI?USgwuf zFNh!XHQZd`b5Z=(o!t)!zdxXzTz<1a=~g27V>Sz_0}@_hjONCdSQ67UQA&zoT0_$w zAfw^*`j^6&RN0Bh)?hHu2PZ*3T&S->upkF)j){5YbdO@G`&GF~<^sxZtdPl+nk5tO z7JRgE?FZ$eDI~l^`7;yy(i+Nduqk;$LCGhlrTMIBgWm3Nx!f9rVKpW98fc@mwO?fQ zt94QFdqk{*P|E1OHf}0+*JR{KfKxdA>U5VA#!&29$VIm6L}FuWvJ9cIRjt#_b^AA? zmim!WZx-lXt_%D1hV=20J+|UJMCgM@>5_V0zm&60z^tqej^BtPzWKbB{U-?*;|HE^ zZjZ+97{}k}-)+oIzs8URV`}T-PhbVT8~A~<b9Yw!Rw^8SwBzJqJV|4--53^nCCOLB z-Geo>!$LwcNZI-43(z~$xD-(IGB4OlLN|G#M^o@KN>?$v>xxO8b>tL#-#Ts~N<Qsq z<CYwKA@gi_Qee?Hwqo0SCpG!k3^Ys@LLFVo**KVtOn{u2_=UkH+agH0tRX@@4!rtZ z)bDP6{#qA)^0<VPvm<XM_qJ_Fl+&F1aWxx;>b1dVhT?(dn=4gItqgX7G9NOHZq&IH zL4Z`28C$)2^UCy37Wl;3+-?nB#;n`tk4%7Dh~%UZ-wN(@dipKKQ(^EiaCQ3B+{nvN zg}aDfx2Dswk8gnzBacjdA^Hr_zRp7*kW)f-9?dbVLYf6J?0K4>r4$v<tImn7-(h(U zKzWgN-PL!w`@oEUI#e8g1yG<bPn52qYnazdcg(BJY+`-Q91{b>6P>Sf<D;X$4>ph@ zGsmdW(d@TCPHVa%GnM8u|B0t}(T{%7s?BTVSvc|N6r-}o>XiV1e_QA&JJ<i>P2W=6 z9E!)!Te)_{ncYv|#qrhwgna1k@R{2e7l66E?kyJt(DlzM(H}vKUdVxcBSpnq(?!9_ zy-VM=MA-hyP9EoN1Rm+5e{0B<b~;u8N1qXLm|K<@_>jIj*ahfxn}XMQw`s-Q)i#Rb z5iy-hYd^1IG+MeiLaTEk>!n=lJ2zaI)-|OQOa;7?fitTO+mRGc-0b3y&$|5=AB<^@ z>R+hI{#5PxtoJM{w=cj%=l>#q+eyu*9T-1E#J^xWAtiTadxvs?6yIz}Q~S8cG-aTR zp9v{Rq)lv=X)a7Dn*+%%(*Bhg@+m+tg}X1s+g-Q-2BF>7&JP!H9(i6DZFGOZ3wHK< z%GEok1IA>oZMLU-wcKn9e%&RqfV~AAsyw8rHbCg2OSI1#44-ZUvKr~V;+iNO52rRJ zOn3@dIPOQu49#!tHqkLG&!J3_#aod(mb(%E#%}PU`A!RB=QUf9fp}ZX*rbYmY&9@o zN*Vuw-&tqnYJz*W;a+95-(T@7SA7G3Ro02k%DBMu)WwJ9EQR*Cxu?mMjvIDQK-C&E zx%7aY;GlHPnd>|;tt*_T^8vu0QVyOt44&X{J`dswz|nQ9V1TR&?fZJ-n=r8k12D%Y z@n<&u(P39M?7JTW@~Y>sNgiG^1KfsE{;+@REz!UA7T+@^kTbq1Kr$U?pviRNSxmIb z3%+`gpxMUFDGa&lRQG6e>)XJ<scu;m>v$3VDz@e5VmfJ0dj^bSJHWOwzQv&e-+S2l z*J1dUmZMOx>A3WcCHJexkKBRzEfyv<bskD-+_d$}Y@4uNs?*#OnUF>9DxD@e1RUkd zqZc#CUBzl=>z8U8C3l+1ArM*e&%csF$*)X9ems#(s%`Qml&*?H9xqK|cUE@f^vL&m z${_?bx$;HSkGs`%9a4QgAo-sr!k@XsPB>L;sHA_wB?d!GnB0`{efWp;q{BUzW9m%# zGDF`bzVyazvvX76NILI;-!3z7Wjx)*TGSi1+G#Iocw=v!@i7@lNVh^V)8qL@!KEDR zk(bCFdI9NmY3aBGb8<kUS?59k21D+(jX&o0DZKL5%(yOr{yxg!ivy7MrVJh^e9cAS z@0tB5UT95ap6^;mOKb~ZccRdG?p2u)WYZmMO^898w|rd^64Dq8N-#~Y<QEzK)H@b^ zAmaAT?QxR1z_L}uQq$lqoy3Kxjs_1??(z5;*k-wGuL|PR8{4$xOj(9#yg7h0>!r{` zEr@$CJ=XuD>AI<@83BtQs+d0BD)x&HT!zfYqmWSgO`~28j>#@*{h98`c!4S!s2vMY z=+_{IgErE@h?<lTZ<!spYA9-O_(jm4X8g`M5Ep#%abo%U?-Q1%@KT;})aJt&#R>Zx zfHNfi_1&32@^0r<PC@RufVUt+`}Ay%$G=QcaDfqxU^V<c#C3b5wKamvd(U+nE-+h< z6UH`qM}s*q$$H2Z!qNl5#AEY=oxKwGU&_m!IY9>v@4yKd0E#^q2twnuW-Ea&lTp8{ ztai8De<1^`62{X)yJWPh0Fr(RF;cz&Ba1sX^R(rGYu%<B#kUTge4g`mSth7KCGjw_ zdeknJnzNfwKwu(@nM7w{$><YXb|O=!;sT3e*s>rA<x~2Dn_2_<{T_32$E1tOVi#P_ zx!k<IlPyO#2<�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 literal 0 HcmV?d00001 diff --git a/doc/tab_audit.png b/doc/tab_audit.png new file mode 100644 index 0000000000000000000000000000000000000000..58d78f1940193acdafadd0172b31191c1a80e5fa GIT binary patch literal 25238 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 AmH+?% literal 0 HcmV?d00001 diff --git a/doc/tab_groups.png b/doc/tab_groups.png new file mode 100644 index 0000000000000000000000000000000000000000..c515febca610482b60e5af4326c2945005799cf4 GIT binary patch literal 31957 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 Klg*RH`2R2HhAwFU literal 0 HcmV?d00001 diff --git a/doc/tab_overview.png b/doc/tab_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..57793602a82bcaf6af962bb3a42688beab14d093 GIT binary patch literal 30494 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 X{YG8plCJ<r1}-xr%cErm*lYg=_X0ly literal 0 HcmV?d00001 diff --git a/doc/tab_sources.png b/doc/tab_sources.png new file mode 100644 index 0000000000000000000000000000000000000000..4666f446dac6c759d5bfaf6c8f3ce6c95e735be5 GIT binary patch literal 23197 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 Qz$So>roo9yb?eao3x&U&<p2Nx literal 0 HcmV?d00001 diff --git a/doc/tab_users.png b/doc/tab_users.png new file mode 100644 index 0000000000000000000000000000000000000000..2bc6e9cee70cd238c9ddeb5a7900b193e870ec92 GIT binary patch literal 38818 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	KK^uOp2)KxeAJQmf9J6Bc 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 O?Ef=YKbLh*2~7YAXRMC^ literal 0 HcmV?d00001 diff --git a/doc/user_edit.png b/doc/user_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..9b8bea511c94bc9838e1c714a757cb3f253bb92a GIT binary patch literal 69485 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 Nkx&#b6EpbuzW_4K-bMfb literal 0 HcmV?d00001 diff --git a/dtml/GRUFFolder_main.dtml b/dtml/GRUFFolder_main.dtml new file mode 100644 index 0000000..feb89fb --- /dev/null +++ b/dtml/GRUFFolder_main.dtml @@ -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"> </td> + <td align="right" valign="top"> + <div class="form-element"> + <form action="&dtml-URL1;/" method="get"> + <dtml-if "_.len(filtered_meta_types) > 1"> + <select class="form-element" name=":action" + onChange="location.href='&dtml-URL1;/'+this.options[this.selectedIndex].value"> + <option value="manage_workspace" disabled>Select type to add...</option> + <dtml-in filtered_meta_types mapping sort=name> + <option value="&dtml.url_quote-action;">&dtml-name;</option> + </dtml-in> + </select> + <input class="form-element" type="submit" name="submit" value=" Add " /> + <dtml-else> + <dtml-in filtered_meta_types mapping sort=name> + <input type="hidden" name=":method" value="&dtml.url_quote-action;" /> + <input class="form-element" type="submit" name="submit" value=" Add &dtml-name;" /> + </dtml-in> + </dtml-if> + </form> + </div> + </td> + </tr> + </table> +</dtml-if> + +<form action="&dtml-URL1;/" name="objectItems" method="post"> +<dtml-if objectItems> +<table width="100%" cellspacing="0" cellpadding="2" border="0"> +<tr class="list-header"> + <td width="5%" align="right" colspan="2"><div + class="list-item"><a href="./manage_main?skey=meta_type<dtml-if + "rkey == ''">&rkey=meta_type</dtml-if>" + onMouseOver="window.status='Sort objects by type'; return true" + onMouseOut="window.status=''; return true"><dtml-if + "skey == 'meta_type' or rkey == 'meta_type'" + ><strong>Type</strong><dtml-else>Type</dtml-if></a></div> + </td> + <td width="50%" align="left"><div class="list-item"><a + href="./manage_main?skey=id<dtml-if + "rkey == ''">&rkey=id</dtml-if>" + onMouseOver="window.status='Sort objects by name'; return true" + onMouseOut="window.status=''; return true"><dtml-if + "skey == 'id' or rkey == 'id'" + ><strong>Name</strong><dtml-else>Name</dtml-if></a></div> + </td> + <td width="15%" align="left"><div class="list-item"><a + href="./manage_main?skey=get_size<dtml-if + "rkey == ''">&rkey=get_size</dtml-if>" + onMouseOver="window.status='Sort objects by size'; return true" + onMouseOut="window.status=''; return true"><dtml-if + "skey == 'get_size' or rkey == 'get_size'" + ><strong>Size</strong><dtml-else>Size</dtml-if></a></div> + </td> + <td width="29%" align="left"><div class="list-item"><a + href="./manage_main?skey=bobobase_modification_time<dtml-if + "rkey == ''">&rkey=bobobase_modification_time</dtml-if + >" + onMouseOver="window.status='Sort objects by modification time'; return true" + onMouseOut="window.status=''; return true"><dtml-if + "skey == 'bobobase_modification_time' or rkey == 'bobobase_modification_time'" + ><strong>Last Modified</strong><dtml-else>Last Modified</dtml-if></a></div> + </td> +</tr> +<dtml-in objectItems sort_expr="skey" reverse_expr="rkey"> +<dtml-if sequence-odd> +<tr class="row-normal"> +<dtml-else> +<tr class="row-hilite"> +</dtml-if> + <td align="left" valign="top" width="16"> + <input type="checkbox" name="ids:list" value="&dtml-sequence-key;" /> + </td> + <td align="left" valign="top" nowrap="1"> + + <dtml-if om_icons> + <a href="&dtml.url_quote-sequence-key;/manage_workspace"> + <dtml-in om_icons mapping> + <img src="&dtml-BASEPATH1;/&dtml.url_quote-path;" alt="&dtml.missing-alt;" + title="&dtml.missing-title;" border="0" /></dtml-in></a> + <dtml-else> + + <dtml-if icon> + <a href="&dtml.url_quote-sequence-key;/manage_workspace"> + <img src="&dtml-BASEPATH1;/&dtml-icon;" alt="&dtml-meta_type;" + title="&dtml-meta_type;" border="0" /></a> + <dtml-else> + + </dtml-if> + + </dtml-if> + + </td> + <td align="left" valign="top"> + <div class="list-item"> + <a href="&dtml.url_quote-sequence-key;/manage_workspace"> + &dtml-sequence-key; <dtml-if title>(&dtml-title;)</dtml-if> + </a> + <dtml-if locked_in_version> + <dtml-if modified_in_version> + <img src="&dtml-BASEPATH1;/p_/locked" + alt="This item has been modified in this version" /> + <dtml-else> + <img src="&dtml-BASEPATH1;/p_/lockedo" + alt="This item has been modified in another version" /> + (<em>&dtml-locked_in_version;</em>) + </dtml-if> + </dtml-if> + </div> + </td> + + <dtml-with sequence-key> + <td> + <div class="list-item"> + <dtml-try> + <dtml-if get_size> + <dtml-let ob_size=get_size> + <dtml-if "ob_size < 1024"> + 1 Kb + <dtml-elif "ob_size > 1048576"> + <dtml-var "ob_size / 1048576.0" fmt="%0.02f"> Mb + <dtml-else> + <dtml-var "_.int(ob_size / 1024)"> Kb + </dtml-if> + </dtml-let> + <dtml-else> + + </dtml-if> + <dtml-except> + + </dtml-try> + </div> + </td> + + <td> + <div class="list-item"> + <dtml-var bobobase_modification_time fmt="%Y-%m-%d %H:%M"> + </div> + </td> + </dtml-with> +</tr> +</dtml-in> +</table> + +<table cellspacing="0" cellpadding="2" border="0"> +<tr> + <td align="left" valign="top" width="16"></td> + <td align="left" valign="top"> + <div class="form-element"> + <dtml-unless dontAllowCopyAndPaste> + <input class="form-element" type="submit" name="manage_renameForm:method" + value="Rename" /> + <input class="form-element" type="submit" name="manage_cutObjects:method" + value="Cut" /> + <input class="form-element" type="submit" name="manage_copyObjects:method" + value="Copy" /> + <dtml-if cb_dataValid> + <input class="form-element" type="submit" name="manage_pasteObjects:method" + value="Paste" /> + </dtml-if> + </dtml-unless> + <dtml-if "_.SecurityCheckPermission('Delete objects',this())"> + <input class="form-element" type="submit" name="manage_delObjects:method" + value="Delete" /> + </dtml-if> + <dtml-if "_.SecurityCheckPermission('Import/Export objects', this())"> + <input class="form-element" type="submit" + name="manage_importExportForm:method" + value="Import/Export" /> + </dtml-if> +<script type="text/javascript"> +<!-- +if (document.forms[0]) { + document.write('<input class="form-element" type="submit" name="selectButton" value="Select All" onClick="toggleSelect(); return false">') + } +//--> +</script> + </div> + </td> +</tr> +</table> + +<dtml-else> +<table cellspacing="0" cellpadding="2" border="0"> +<tr> +<td> +<div class="std-text"> +There are currently no items in <em>&dtml-title_or_id;</em> +<br /><br /> +</div> +<dtml-unless dontAllowCopyAndPaste> +<dtml-if cb_dataValid> +<div class="form-element"> +<input class="form-element" type="submit" name="manage_pasteObjects:method" + value="Paste" /> +</div> +</dtml-if> +</dtml-unless> +<dtml-if "_.SecurityCheckPermission('Import/Export objects', this())"> +<input class="form-element" type="submit" + name="manage_importExportForm:method" value="Import/Export" /> +</dtml-if> +</td> +</tr> +</table> +</dtml-if> +</form> + +<dtml-if update_menu> +<script type="text/javascript"> +<!-- +window.parent.update_menu(); +//--> +</script> +</dtml-if> + +<dtml-var manage_page_footer> + + + + + + + + + + + + + diff --git a/dtml/GRUF_audit.zpt b/dtml/GRUF_audit.zpt new file mode 100644 index 0000000..26dfdc0 --- /dev/null +++ b/dtml/GRUF_audit.zpt @@ -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)"> </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 index 0000000..7641a21 --- /dev/null +++ b/dtml/GRUF_contents.zpt @@ -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"> + <a href="Groups/acl_users/manage_workspace" tal:content="here/Groups/acl_users/meta_type">Type</a> + </td> + <td></td> + <td bgcolor="#EEEEEE"> </td> + <td bgcolor="#EEEEEE"> + <table border="0"> + <tr> + <td align="left"> + <input type="hidden" name="source_rec.id:records" value="Groups" /> + <select name="source_rec.new_factory:records"> + <option value="">-- Select your source type --</option> + <tal:block tal:repeat="source here/listAvailableUserSources"> + <option value="" + tal:condition="python:source[0] != path('here/Groups/acl_users/meta_type')" + tal:attributes="value python:source[1]"> + <span tal:replace="python:source[0]">name</span> + </option> + </tal:block> + </select> + </td> + <td align="right"> + <input type="submit" name="replaceUserSource:action" value="Ok" /> + </td> + </tr> + </table> + </td> + <td class="list-item">(forbidden)</td> + <td bgcolor="#EEEEEE" class="list-item"> + <input type="checkbox" name="id" value="Groups" />I'm sure + </td> + </tr> + + + <!-- Users sources row --> + <tr tal:repeat="source here/listUserSourceFolders"> + <th class="list-header">Users source #<span tal:replace="repeat/source/number">1</span></th> + <td bgcolor="#EEEEEE" tal:condition="source/isValid"> + <img src="" + tal:attributes="src source/acl_users/icon; + title source/acl_users/meta_type;"> + <a href="" + tal:attributes=" + href string:${source/acl_users/absolute_url}/manage_workspace; + title source/acl_users/meta_type;" + tal:content="source/acl_users/title|source/acl_users/meta_type">Type</a> + <tal:block condition="not:source/isEnabled"> + <font color="red"><i>(disabled)</i></font> + </tal:block> + </td> + <td bgcolor="#EEEEEE" tal:condition="not:source/isValid"> + <font color="red"><strong><i>(invalid or broken)</i></strong></font> + </td> + <td bgcolor="#EEEEEE" align="center"> + <a tal:condition="not:repeat/source/start" + tal:attributes="href string:${here/absolute_url}/moveUserSourceUp?id=${source/getUserSourceId}" + href=""><img src="img_up_arrow" border="0" alt="Move up"></a> + <span tal:condition="repeat/source/start"><img src="img_up_arrow_grey" border="0" alt="Move up"></span> + + <a tal:condition="not:repeat/source/end" + tal:attributes="href string:${here/absolute_url}/moveUserSourceDown?id=${source/getUserSourceId}" + href=""><img src="img_down_arrow" border="0" alt="Move down"></a> + <span tal:condition="repeat/source/end"><img src="img_down_arrow_grey" border="0" alt="Move down"></span> + </td> + <td bgcolor="#EEEEEE"> + <font size="-2"> + <a + tal:condition="source/isEnabled" + tal:attributes="href string:${here/absolute_url}/toggleSource?src_id=${source/getUserSourceId}" + >Disable + </a> + <a + tal:attributes="href string:${here/absolute_url}/toggleSource?src_id=${source/getUserSourceId}" + tal:condition="not: source/isEnabled" + >Enable + </a> + </font> + </td> + <td bgcolor="#EEEEEE"> + <table border="0"> + <tr> + <td align="left"> + <input type="hidden" name="source_rec.id:records" value="" tal:attributes="value source/getUserSourceId" /> + <select name="source_rec.new_factory:records"> + <option value="">-- Select your source type --</option> + <tal:block tal:repeat="new_source here/listAvailableUserSources"> + <option value="" + tal:condition="python:new_source[0] != path('source/acl_users/meta_type')" + tal:attributes="value python:new_source[1]"> + <span tal:replace="python:new_source[0]">name</span> + </option> + </tal:block> + </select> + </td> + <td align="right"> + <input type="submit" name="replaceUserSource:action" value="Ok" /> + </td> + </tr> + </table> + </td> + <td bgcolor="#EEEEEE" tal:condition="python:repeat['source'].length > 1" class="list-item"> + <input + type="submit" + name="deleteUserSource:action" + value="Delete" /> + </td> + <td tal:condition="python:not repeat['source'].length > 1" class="list-item"> + (forbidden) + </td> + <td bgcolor="#EEEEEE" class="list-item"> + <input type="checkbox" name="id" value="" tal:attributes="value source/getUserSourceId" />I'm sure + </td> + </tr> + + + <!-- Blank row --> + <tr> + <td class="list-item" colspan="6"> </td> + </tr> + + <!-- New sources row --> + <tr> + <th class="list-header">Add...</th> + <td colspan="6" class="list-item"> + <select name="factory_uri"> + <option value="">-- Select your source type --</option> + <option value="" tal:repeat="source here/listAvailableUserSources" tal:attributes="value python:source[1]"> + <span tal:replace="python:source[0]">name</span> + </option> + </select> + <input type="submit" name="addUserSource:method" value="Add" /> + </td> + </tr> + </table> + </form> + + </ol> + + <tal:block condition="here/hasLDAPUserFolderSource"> + <h4>Special operations</h4> + <ol> + <p class="form-help"> + To manage groups with a LDAPUserFolder, one must map LDAP groups to Zope Roles.<br /> + You can do this mapping manually or click this button to have it done automatically.<br /> + Please not that any previously existing ldap-group - to - zope-role mapping may be lost. + </p> + <p class="form-help"> + To help you in this task, you can have a look at the following table, which summs up<br /> + the mappings done (or not done!) in LDAPUserFolder. + </p> + + <table> + <thead> + <th>GRUF group</th> + <th>LDAP group</th> + </thead> + <tbody> + <tr tal:repeat="group_info here/listLDAPUserFolderMapping"> + <td tal:content="python:group_info[0]"></td> + <td tal:content="python:group_info[1]"></td> + </tr> + </tbody> + </table> + <form action="updateLDAPUserFolderMapping"> + <input type="submit" value="Update LDAP mapping" /> + </form> + </ol> + </tal:block> + + +<h1 tal:replace="structure here/manage_page_footer">Footer</h1> diff --git a/dtml/GRUF_groups.zpt b/dtml/GRUF_groups.zpt new file mode 100644 index 0000000..64fe589 --- /dev/null +++ b/dtml/GRUF_groups.zpt @@ -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"> + <a href="Groups/acl_users/manage_workspace" tal:content="here/Groups/acl_users/meta_type">Type</a> + </td> + </tr> + </table> + </ol> + + <h4>Groups management</h4> + <form action="" method="POST" tal:attributes="action here/absolute_url"> + <ol> + <table cellspacing="10" width="90%" tal:define="groups here/getGroups"> + <tr> + <!-- Groups selection --> + <td valign="top"> + <table bgcolor="#EEEEEE" width="100%"> + <tr class="list-header" tal:condition="groups"> + <th> </th> + <th>Group</th> + <th class="list-header">Member <br>of groups</th> + <th class="list-header">Implicitly <br>member of*</th> + <th class="list-header">Has roles</th> + <th class="list-header">Implicitly <br>has roles**</th> + </tr> + + <tr + tal:repeat="group groups" class="" tal:attributes="class python:test(path('repeat/group/odd'), 'row-hilite', 'row-normal')" + > + <div tal:define=" + label_groups python:group.getGroups(); + label_groups_no_recurse python:group.getImmediateGroups(); + label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups); + groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse); + groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse); + roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), group.getRoles()); + roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), group.getUserRoles()); + roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles);" + tal:omit-tag=""> + <td><div class="list-item"><input type="checkbox" name="groups:list" value="" tal:attributes="value group"></td> + <td> + <div class="list-item"> + <img src="img_group"> + <strong tal:content="structure group/asHTML"> + </strong> + </td> + <td class="list-item"> + <span tal:repeat="group groups_no_recurse" > + <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + <td class="list-item"> + <span tal:repeat="group groups_recurse" > + <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_no_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + </div> + </tr> + + <!-- New user --> + <tr> + <td><div class="list-item"> </div></td> + <td><div class="list-item">Create groups:<br /><textarea name="new_groups:lines" cols="20" rows="3"></textarea></div></td> + <td colspan="4"> + <div class="list-item"> + Newly created groups will be affected groups and roles according to the table below. + </div> + </td> + </tr> + <tr> + <td colspan="2" align="center"> + <input type="submit" name="changeOrCreateGroups:method" value="Create" /> + + <input type="submit" name="deleteGroups:method" value="Delete" /> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td align="center"> + <div class="list-item"> + Select one or more users in the upper table, select one or more groups / roles in the table below + and click "Change" to affect groups / roles to these users. + </div> + </td> + </tr> + <tr> + <td valign="top" align="center" colspan="6"> + <table bgcolor="#EEEEEE"> + <tr> + <td valign="top"> + <!-- Groups selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Affect groups</th> + </tr> + + <tr tal:repeat="group here/getGroups"> + <td> + <input type="checkbox" name="nested_groups:list" value="" tal:attributes="value group"> + </td> + <td> + <div class="list-item" tal:content="structure group/asHTML"></div> + </td> + </tr> + + <!-- "(None)" item --> + <tr> + <td><div class="list-item"><input type="checkbox" name="nested_groups:list" value="__None__"></div></td> + <td><div class="list-item"><i>(None)</i></div></td> + </tr> + </table> + </td> + <td valign="top"> + <!-- Roles selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Affect roles</th> + </tr> + + <tr tal:repeat="role here/valid_roles"> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <input type="checkbox" name="roles:list" value="" tal:attributes="value role"> + </td> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div> + </td> + </tr> + + <!-- "(None)" item --> + <tr> + <td><div class="list-item"><input type="checkbox" name="roles:list" value="__None__"></div></td> + <td><div class="list-item"><i>(None)</i></div></td> + </tr> + </table> + </td> + </tr> + <tr> + <td colspan="2" align="middle"><input type="submit" name="changeOrCreateGroups:method" value="Change" /></td> + </table> + </td> + </tr> + </table> + + + + + <tr tal:replace="nothing"> + <td valign="top" bgcolor="#EEEEEE"> + <!-- Groups selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Affect groups</th> + </tr> + + <tr tal:repeat="group here/getGroups"> + <td> + <input type="checkbox" name="nested_groups:list" value="" tal:attributes="value group"> + </td> + <td> + <div class="list-item" tal:content="structure group/asHTML"></div> + </td> + </tr> + + <!-- "(None)" item --> + <tr> + <td><div class="list-item"><input type="checkbox" name="nested_groups:list" value="__None__"></div></td> + <td><div class="list-item"><i>(None)</i></div></td> + </tr> + </table> + + <br> + + <!-- Roles selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Affect roles</th> + </tr> + + <tr tal:repeat="role here/valid_roles"> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <input type="checkbox" name="roles:list" value="" tal:attributes="value role"> + </td> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div> + </td> + </tr> + <!-- "(None)" item --> + <tr> + <td><div class="list-item"><input type="checkbox" name="roles:list" value="__None__"></div></td> + <td><div class="list-item"><i>(None)</i></div></td> + </tr> + </table> + </td> + </tr> + + <p class="form-help"> + * According to the groups inheritance, this group is also recursively member of these groups. <br />This is what we call nested groups. + </p> + <p class="form-help"> + ** Accorded to the groups inheritance, this group also has these roles - even if they are not defined explicitly on it. + </p> + + </ol> + </form> + + + <h4>Instructions</h4> + <ol> + + <p class="form-help"> + To change roles for one or several groups, select them in the left form, select the roles you want to give them in the form on the right and click "Change".<br /> + You can also create one or several groups by filling the text area (one group per line). the "Change" button will create them with the roles you've selected.<br /> + If you are fed up with some groups, you can delete them by selecting them and clicking the "Delete" button. + </p> + <p class="form-help"> + If you do not select any role, roles won't be reseted for the selected groups.<br /> + If you do not select any group, groups won't be reseted for the selected groups.<br /> + To explicitly reset groups or roles, just click the "(None)" entry (and no other entry). + </p> + </ol> + + <h4>Important notice / disclaimer</h4> + + <ol> + <p class="form-help"> + This form uses the regular Zope Security API from the underlying user folders. However, you may experience problems with some + of them, especially if they are not tuned to allow user adding. For example, an LDAPUserFolder can be configured to disable + users management. In case this form doesn't work, you'll have to do things by hand within the 'Users' and 'Groups' GRUF folders. + </p> + + <p class="form-help"> + This is not a GRUF limitation ! :-) + </p> + </ol> + +<h1 tal:replace="structure here/manage_page_footer">Footer</h1> diff --git a/dtml/GRUF_newusers.zpt b/dtml/GRUF_newusers.zpt new file mode 100644 index 0000000..93a1092 --- /dev/null +++ b/dtml/GRUF_newusers.zpt @@ -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 index 0000000..9db51db --- /dev/null +++ b/dtml/GRUF_overview.zpt @@ -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"> <strong tal:content="structure user/asHTML"></strong> + </div> + </td> + <td> + <!-- Groups --> + <div class="list-item"> + <span tal:repeat="group groups_no_recurse" + ><span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span></span + ><span tal:condition="python:groups_no_recurse and groups_recurse">,</span> + <span tal:repeat="group groups_recurse" > + <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </div> + </td> + <td> + <!-- Roles --> + <div class="list-item"> + <span tal:repeat="role roles_no_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + <span tal:condition="python:roles_no_recurse and roles_recurse">, </span> + <span tal:repeat="role roles_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + </tr> + </tal:block> + </table> + + <table tal:condition="not:users"> + <tr> + <td class="row-hilite" colspan="3"> + <p> + No user available. This happens either if you have no users defined or if + the underlying UserFolder cannot retreive the entire users list. + </p> + </td> + </tr> + </table> + </tal:block> + </tal:block> +</ol> + + +<!-- Legend --> +<h4>Legend</h4> +<ol> + <p> + Just to make things clearer: <br> + <font color="" tal:attributes="color here/user_color"><img src="img_user"> Users appear this way</font><br /> + <font color="" tal:attributes="color here/group_color"><img src="img_group"> Groups appear this way</font><br /> + <font color="" tal:attributes="color here/group_color"><i><img src="img_group"> Nested groups (ie. groups inside groups) appear this way</i></font><br /> + <font color="" tal:attributes="color here/role_color">User roles appear this way</font><br /> + <font color="" tal:attributes="color here/role_color"><i>Nested roles (ie. roles set on a group a user or group belongs to) appear this way</i></font><br /> + </p> + <p class="form-help">In management forms, items only non-italic items can be set/unset directly. Italic items are dependencies.</p> +</ol> + +<dtml-var manage_page_footer> + diff --git a/dtml/GRUF_user.zpt b/dtml/GRUF_user.zpt new file mode 100644 index 0000000..56937b7 --- /dev/null +++ b/dtml/GRUF_user.zpt @@ -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"> + <img src="" alt="kind" tal:attributes="src icon; alt kind" align="middle"> + <strong tal:condition="user/isGroup" tal:content="structure string:${user/asHTML} (Group)">toto group management</strong> + <strong tal:condition="not: user/isGroup" tal:content="structure string:${user/asHTML} (User)">toto user management</strong> + </div> + + + <h4>Settings</h4> + + <form action="" method="POST" tal:attributes="action here/absolute_url"> + <tal:block tal:define=" + label_groups python:user.getGroups(); + label_groups_no_recurse python:user.getImmediateGroups(); + label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups); + groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse); + groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse); + roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles()); + roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles()); + roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles) + "> + <ol> + <table cellspacing="10"> + <tr> + <!-- User info --> + <input type="hidden" name="user" value="" tal:attributes="value user/getUserName"> + <td valign="top"> + <table bgcolor="#EEEEEE"> + <tr> + <th class="list-header"><span tal:replace="kind" /> name</th> + <td class="list-item"> + <strong tal:content="structure user/asHTML"> + </strong> + </td> + </tr> + <tr> + <th class="list-header">Member of groups</th> + <td class="list-item"> + <span tal:repeat="group groups_no_recurse" > + <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + </tr> + <tr> + <th class="list-header">Implicitly member of groups</th> + <td class="list-item"> + <span tal:repeat="group groups_recurse" > + <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + </tr> + <tr> + <th class="list-header">Has roles</th> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_no_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + </tr> + <tr> + <th class="list-header">Implicitly has roles (from groups)</th> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + </tr> + <tr> + <td colspan="4" align="center"><br> + <input type="submit" name="changeUser:method" value="Change" /> + <tal:block tal:replace="nothing"> + XXX have to make this work again + + <input type="submit" name="deleteUser:method" value="Delete" /> + <br> + </tal:block> + </td> + </tr> + </table> + </td> + + <td valign="middle"> + => + </td> + + <td valign="top"> + <table bgcolor="#EEEEEE"> + <tr> + <td> + <!-- Groups selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Set groups</th> + </tr> + + <tr tal:repeat="group here/getGroups"> + <td> + <input type="checkbox" name="groups:list" value="" checked="" + tal:condition="python: group.getUserName() != user.getUserName()" + tal:attributes=" + value group/getUserName; + checked python:test(group.getId() in user.getGroupIds(), '1', '')"> + </td> + <td> + <div class="list-item" tal:content="structure group/asHTML"></div> + </td> + </tr> + </table> + + <br> + + <!-- Roles selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Set roles</th> + </tr> + + <tr tal:repeat="role here/valid_roles"> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <input type="checkbox" name="roles:list" value="" checked="" + tal:attributes="value role; checked python:test(role in user.getUserRoles(), '1', '')"> + </td> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div> + </td> + </tr> + </table> + </td> + </tr> + </table> + </ol> + </tal:block> + + </form> + + + <tal:block tal:condition="nothing|user/isGroup"> + XXX TODO ! XXX + <h4>Group contents</h4> + <ol> + <table bgcolor="#EEEEEE" tal:define="content python:list(user.getImmediateGroups())"> + <tr class="list-header"> + <th>Group/User</th> + <th class="list-header">Member <br>of groups</th> + <th class="list-header">Implicitly <br>member <br>of groups</th> + <th class="list-header">Has roles</th> + <th class="list-header">Implicitly <br>has roles <br>(from groups)</th> + </tr> + + <tr + tal:repeat="user python:content" class="" tal:attributes="class python:test(path('repeat/user/odd'), 'row-hilite', 'row-normal')" + > + <div tal:define=" + label_groups python:user.getGroups(); + label_groups_no_recurse python:user.getImmediateGroups(); + label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups); + groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse); + groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse); + roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles()); + roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles()); + roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles);" + tal:omit-tag=""> + <td class="list-item"> + <span tal:repeat="group groups_no_recurse" > + <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + <td class="list-item"> + <span tal:repeat="group groups_recurse" > + <span tal:replace="structure python:user.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_no_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + </div> + </tr> + </table> + </ol> + </tal:block> + + + <h4>Instructions</h4> + <ol> + <p class="form-help"> + To change roles for a <span tal:replace="kind" />, + select the roles you want to give it and the groups it belongs to in the forms on the right and click "Change".<br /> + </p> + </ol> + + <h4>Important notice / disclaimer</h4> + + <ol> + <p class="form-help"> + This form uses the regular Zope Security API from the underlying user folders. However, you may experience problems with some + of them, especially if they are not tuned to allow user adding. For example, an LDAPUserFolder can be configured to disable + users management. In case this form doesn't work, you'll have to do things by hand within the 'Users' and 'Groups' GRUF folders. + </p> + + <p class="form-help"> + This is not a GRUF limitation ! :-) + </p> + </ol> + + </tal:block> + + <h1 tal:replace="structure here/manage_page_footer">Footer</h1> + + diff --git a/dtml/GRUF_users.zpt b/dtml/GRUF_users.zpt new file mode 100644 index 0000000..b80223d --- /dev/null +++ b/dtml/GRUF_users.zpt @@ -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"> + + <a href="" + tal:attributes=" + href string:${source/acl_users/absolute_url}/manage_workspace; + title meta_type" + tal:content="title_or_id">Title</a> + <tal:block condition="not:source/isEnabled"> + <font color="red"><i>(disabled)</i></font> + </tal:block> + </td> + <td bgcolor="#EEEEEE" tal:condition="not:source/isValid"> + <font color="red"><strong><i>(invalid or broken)</i></strong></font> + </td> + </tr> + </table> + </ol> + + <tal:block + tal:condition="not: search_userid" + tal:define="global search_userid request/search_userid|nothing" + > + <tal:block tal:define="global users here/getPureUsers"> + </tal:block> + </tal:block> + <tal:block tal:condition="search_userid"> + <tal:block + tal:define=" + uid search_userid; + global users python:[ here.getUser(uid) for uid in here.searchUsersById(uid) if uid ]; + "> + </tal:block> + </tal:block> + + <h4>Search</h4> + <ol> + <div + tal:define="have_users python: len(users);"> + <div class="list-item" tal:condition="python: not have_users and not search_userid"> + No user available. This happens either if you have no users defined or if + the underlying UserFolder cannot retreive the entire users list (for example, LDAPUserFolder + is limited in results size). + </div> + <div class="list-item"> + Some more users may be available but do not show up there.. This happens if + the underlying UserFolder cannot retreive the entire users list (for example, + LDAPUserFolder is limited in results size and will return only cached users). + </div> + <div class="list-item"> + You can search users giving part of their id with this form. + </div> + <div> + <form action="" tal:attributes="action template/absolute_url"> + <b>User name:</b> + <input name="search_userid" type="text" tal:attributes="value search_userid" /> + <input type="submit" value="Search" /> + </form> + </div> + </div> + </ol> + + <h4 tal:condition="not: search_userid">Users management</h4> + <h4 tal:condition="search_userid">Search results</h4> + <form action="" method="POST" tal:attributes="action request/URL1"> + <ol> + <div tal:condition="python: not users and search_userid"> + No user found. + </div> + <table cellspacing="10" width="90%"> + <tr> + <!-- Users selection --> + <td valign="top"> + <table bgcolor="#EEEEEE" width="100%"> + <tr class="list-header" tal:condition="users"> + <th> </th> + <th>User</th> + <th class="list-header">Member <br>of groups</th> + <th class="list-header">Implicitly <br>member of*</th> + <th class="list-header">Has roles</th> + <th class="list-header">Implicitly <br>has roles**</th> + </tr> + + <tr + tal:repeat="user users" + class="" + tal:attributes="class python:test(path('repeat/user/odd'), 'row-hilite', 'row-normal')" + > + <div tal:condition="user" + tal:omit-tag="" + x:comment="We ignore empty/invalid users" + > + <div tal:define=" + label_groups python:user.getGroups(); + label_groups_no_recurse python:user.getGroups(no_recurse = 1); + label_groups_recurse python:filter(lambda x: x not in label_groups_no_recurse, label_groups); + groups_no_recurse python:map(lambda x: here.getUser(x), label_groups_no_recurse); + groups_recurse python:map(lambda x: here.getUser(x), label_groups_recurse); + roles python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getRoles()); + roles_no_recurse python:filter(lambda x: x not in ('Authenticated', 'Shared'), user.getUserRoles()); + roles_recurse python:filter(lambda x: x not in roles_no_recurse, roles);" + tal:omit-tag=""> + <td><div class="list-item"><input type="checkbox" name="users:list" value="" tal:attributes="value user"></td> + <td> + <div class="list-item"> + <img src="img_user" /> + <strong tal:content="structure user/asHTML"> + </strong> + </td> + <td class="list-item"> + <span tal:repeat="group groups_no_recurse" > + <span tal:replace="structure group/asHTML"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + <td class="list-item"> + <span tal:repeat="group groups_recurse" > + <span tal:replace="structure python:group.asHTML(implicit=1)"></span><span tal:condition="not:repeat/group/end">, </span> + </span> + </td> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_no_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <span tal:replace="role"></span><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + <td class="list-item"> + <div class="list-item"> + <span tal:repeat="role roles_recurse" > + <font color="" + tal:attributes="color here/role_color"> + <i><span tal:replace="role"></span></i><span tal:condition="not:repeat/role/end">, </span> + </font> + </span> + </div> + </td> + </div> + </div> + </tr> + <tr> + <td colspan="5"> + <input type="submit" name="deleteUsers:method" value="Delete" /><br /> + You can also change group / roles with the form below. + </td> + </tr> + </table> + + + <div tal:condition="python: not search_userid" + tal:define="have_users python: len(users);"> + <div class="list-item" tal:condition="not: have_users"> + No user available. This happens either if you have no users defined or if + the underlying UserFolder cannot retreive the entire users list (for example, LDAPUserFolder + is limited in results size).<br /> + Use the above search form to search for specific users. + </div> + </div> + </ol> + + <!-- New user --> + <h4>User creation</h4> + <ol> + <table> + <tr> + <td><div class="list-item"> </div></td> + <td> + <div class="list-item">Batch user creation list:</div> + </td> + </tr> + <tr> + <td><div class="list-item"> </div></td> + <td> + <div class="list-item"> + <textarea name="new_users:lines" cols="20" rows="3"></textarea> + </div> + </td> + <td colspan="4"> + <div class="list-item" valign="top"> + Newly created users will be affected groups and roles according to the table below. + </div> + </td> + </tr> + <tr> + <td><div class="list-item"> </div></td> + <td> + <div class="list-item">Default password:</div> + </td> + </tr> + <tr> + <td><div class="list-item"> </div></td> + <td> + <div class="list-item"> + <input name="default_password:string" size="20" /> + </div> + </td> + <td colspan="4"> + <div class="list-item"> + Fill in this field to specify a default password for new users, + or leave it empty to let GRUF generate random ones. + </div> + </td> + </tr> + <tr> + <td colspan="2" align="center"> + <input type="submit" name="changeOrCreateUsers:method" value="Create" /> + </td> + </tr> + </table> + </ol> + + + <h4>Roles / groups management</h4> + <ol> + <table> + <tr> + <td align="center"> + <div class="list-item"> + Select one or more users in the upper table, select one or more groups / roles in the table below + and click "Change" to affect groups / roles to these users. + </div> + </td> + </tr> + <tr> + <td valign="top" align="center" colspan="6"> + <table bgcolor="#EEEEEE"> + <tr> + <td valign="top"> + <!-- Groups selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Affect groups</th> + </tr> + + <tr tal:repeat="group here/getGroups"> + <td> + <input type="checkbox" name="groups:list" value="" tal:attributes="value group"> + </td> + <td> + <div class="list-item" tal:content="structure group/asHTML"></div> + </td> + </tr> + + <!-- "(None)" item --> + <tr> + <td><div class="list-item"><input type="checkbox" name="nested_groups:list" value="__None__"></div></td> + <td><div class="list-item"><i>(None)</i></div></td> + </tr> + </table> + </td> + <td valign="top"> + <!-- Roles selection --> + <table width="100%"> + <tr class="list-header"> + <th colspan="2">Affect roles</th> + </tr> + + <tr tal:repeat="role here/valid_roles"> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <input type="checkbox" name="roles:list" value="" tal:attributes="value role"> + </td> + <td tal:condition="python:role not in ('Authenticated', 'Anonymous', 'Shared')"> + <div class="list-item"><font color="" tal:attributes="color here/role_color" tal:content="role">Role</font></div> + </td> + </tr> + + <!-- "(None)" item --> + <tr> + <td><div class="list-item"><input type="checkbox" name="roles:list" value="__None__"></div></td> + <td><div class="list-item"><i>(None)</i></div></td> + </tr> + </table> + </td> + </tr> + <tr> + <td colspan="2" align="middle"><input type="submit" name="changeOrCreateUsers:method" value="Change" /></td> + </table> + </td> + </tr> + </table> + + <p class="form-help"> + If you do not select a role, roles won't be reset for the selected users.<br /> + If you do not select a group, groups won't be reset for the selected users.<br /> + To explicitly reset groups or roles, just click the "(None)" entry (and no other entry). + </p> + + <p class="form-help"> + * According to the groups inheritance, this group is also recursively member of these groups. <br />This is what we call nested groups. + </p> + <p class="form-help"> + ** Accorded to the groups inheritance, this group also has these roles - even if they are not defined explicitly on it. + </p> + + </ol> + </form> + + + <h4>Instructions</h4> + <ol> + <p class="form-help"> + To change roles for one or several users, select them in the left form, + select the roles you want to give them and the groups they belong to in the forms on the right and click "Change".<br /> + You can also create one or several users by filling the text area (one user per line). + The "Change" button will create them with the roles and group affectation you've selected. + A random password will be generated for them, and it will be shown in a page so that you can click/paste them somewhere.<br /> + If you want to kill some users, you can delete them by selecting them and clicking the "Delete" button. + </p> + </ol> + + <h4>Important notice / disclaimer</h4> + + <ol> + <p class="form-help"> + This form uses the regular Zope Security API from the underlying user folders. However, you may experience problems with some + of them, especially if they are not tuned to allow user adding. For example, an LDAPUserFolder can be configured to disable + users management. In case this form doesn't work, you'll have to do things by hand within the 'Users' and 'Groups' GRUF folders. + </p> + + <p class="form-help"> + This is not a GRUF limitation ! :-) + </p> + </ol> + + <h1 tal:replace="structure here/manage_page_footer">Footer</h1> diff --git a/dtml/GRUF_wizard.zpt b/dtml/GRUF_wizard.zpt new file mode 100644 index 0000000..6303879 --- /dev/null +++ b/dtml/GRUF_wizard.zpt @@ -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 index 0000000..7a92739 --- /dev/null +++ b/dtml/addLDAPGroupFolder.dtml @@ -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> </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 index 0000000..fd0fc6c --- /dev/null +++ b/dtml/configureGroupsTool.dtml @@ -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 index 0000000..c89c385 --- /dev/null +++ b/dtml/explainGroupDataTool.dtml @@ -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 index 0000000..fe1ae61 --- /dev/null +++ b/dtml/explainGroupsTool.dtml @@ -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 index 0000000..432b875 --- /dev/null +++ b/dtml/groups.dtml @@ -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"> </td> + <td><div class="form-label"> Friendly Name </div></td> + <td><div class="form-label"> Object Class </div></td> + <td><div class="form-label"> Distinguished Name </div></td> + </tr> + </dtml-if> + + <dtml-if sequence-odd> + <tr class="row-normal"> + <dtml-else> + <tr class="row-hilite"> + </dtml-if> + <td align="left" valign="top" width="16"> + <input type="checkbox" name="dns:list" value="&dtml-sequence-item;" /> + </td> + <td><div class="form-text"> + <dtml-var name="sequence-key"> + </div></td> + <td><div class="form-text"> + <dtml-var expr="getGroupType( _['sequence-item'] )"> + </div></td> + <td><div class="form-text"> + <dtml-var name="sequence-item" size="60" etc="..."> + </div></td> + </tr> + + <dtml-if name="sequence-end"> + <tr> + <td align="left" valign="top" width="16"> </td> + <td align="left" valign="top" colspan="2"><div class="form-element"> + <input class="form-element" type="submit" + name="manage_deleteGroups:method" + value="Delete" /> + </div></td> + </tr> + </table> + </form> + </dtml-if> + +<dtml-else> + <br /> + <div class="form-label"> + No groups found. + Please check the settings "Group base DN" and "Groups search scope" + and make sure your LDAP tree contains suitable group records. + </div> + +</dtml-in> + +<p><br></p> + +<form action="manage_addGroup" method="post"> + + <table cellspacing="0" cellpadding="2" width="95%"> + + <tr class="section-bar"> + <td colspan="2" align="left" valign="top"><div class="form-label"> + Add Group + </div></td> + </tr> + + <tr> + <td colspan="2" align="left" valign="top"><div class="form-text"> + Add a new group on this LDAP branch by specifying a group name + and hitting "Add". + The name is a "friendly" name, meaning it + is not a dn or does not contain any LDAP-sepecific elements. + </div></td> + </tr> + + <tr><td colspan="2"> </td></tr><tr> + <td align="left" valign="absmiddle"><div class="form-label"> + Group Name + </div></td> + <td align="LEFT" valign="TOP"> + <input type="TEXT" name="newgroup_name" size="50" + value="MyGroup" /> + </td> + </tr> + + <tr> + <td align="left" valign="absmiddle"><div class="form-label"> + Group object class + </div></td> + <td align="LEFT" valign="TOP"> + <select name="newgroup_type"> + <option value="groupOfUniqueNames"> groupOfUniqueNames </option> + <option value="groupOfNames"> groupOfNames </option> + <option value="accessGroup"> accessGroup </option> + <option value="group"> group </option> + </select> + </td> + </tr> + + <tr> + <td align="left" valign="top" colspan="2"> + <input class="form-element" type="SUBMIT" value=" Add " /> + </td> + </tr> + + </table> + +</form> + +<p><hr></p> + +<table cellspacing="0" cellpadding="2" width="95%"> + <tr> + <td align="left" valign="top"><div class="form-text"> + This section determines if LDAP groups are mapped to Zope roles + and what they map to. + </div></td> + </tr> +</table> + +<br /> + +<dtml-in getGroupMappings> + + <dtml-if name="sequence-start"> + <form action="&dtml-URL1;" method="post"> + <table border="0" cellpadding="2" cellspacing="0" width="95%"> + <tr class="list-header"> + <td align="left" valign="top" width="16"> </td> + <td><div class="form-label"> LDAP Group </div></td> + <td><div class="form-label"> Zope Role </div></td> + </tr> + </dtml-if> + + <dtml-if sequence-odd> + <tr class="row-normal"> + <dtml-else> + <tr class="row-hilite"> + </dtml-if> + <td align="left" valign="top" width="16"> + <input type="checkbox" name="group_names:list" value="&dtml-sequence-key;" /> + </td> + <td><div class="form-text"> &dtml-sequence-key; </div></td> + <td><div class="form-text"> &dtml-sequence-item; </div></td> + </tr> + + <dtml-if name="sequence-end"> + <tr> + <td align="left" valign="top" width="16"> </td> + <td align="left" valign="top" colspan="2"><div class="form-element"> + <input class="form-element" type="submit" + name="manage_deleteGroupMappings:method" + value="Delete" /> + </div></td> + </tr> + </table> + </dtml-if> + +<dtml-else> + <p>(No group mappings specified at this time.)</p> + +</dtml-in> + +<p> </p> + +<form action="&dtml-URL1;" method="post"> + + <table cellspacing="0" cellpadding="2" width="95%"> + + <tr class="section-bar"> + <td colspan="4" align="left" valign="top"><div class="form-label"> + Add LDAP group to Zope role mapping + </div></td> + </tr> + + <tr> + <td align="left" valign="absmiddle"><div class="form-label"> + Map this LDAP Group... + </div></td> + <td align="LEFT" valign="TOP"> + <select name="group_name"> + <dtml-in getGroups sort> + <option>&dtml-sequence-key;</option> + </dtml-in> + </select> + </td> + <td align="left" valign="absmiddle"><div class="form-label"> + ... to this Zope Role + </div></td> + <td align="LEFT" valign="TOP"> + <select name="role_name"> + <dtml-in expr="_.reorder( valid_roles() + , without=( 'Anonymous', 'Authenticated', 'Owner' ) + )" sort> + <option>&dtml-sequence-item;</option> + </dtml-in> + </select> + </td> + </tr> + + <tr> + <td align="left" valign="top" colspan="4"> + <input class="form-element" type="SUBMIT" value=" Add " + name="manage_addGroupMapping:method"> + </td> + </tr> + + </table> + +</form> + +<dtml-var manage_page_footer> diff --git a/dtml/roles.png b/dtml/roles.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2456b4688db7bbb57ae03eaed95a63bd12ca27 GIT binary patch literal 26916 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 CfCmi# literal 0 HcmV?d00001 diff --git a/global_symbols.py b/global_symbols.py new file mode 100644 index 0000000..83d5321 --- /dev/null +++ b/global_symbols.py @@ -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 index 0000000..f3d74a9 --- /dev/null +++ b/interfaces/.cvsignore @@ -0,0 +1,2 @@ +*.pyc +*~ diff --git a/interfaces/IUserFolder.py b/interfaces/IUserFolder.py new file mode 100644 index 0000000..9892274 --- /dev/null +++ b/interfaces/IUserFolder.py @@ -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 index 0000000..cb3bafe --- /dev/null +++ b/interfaces/__init__.py @@ -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 index 0000000..c0786a2 --- /dev/null +++ b/interfaces/portal_groupdata.py @@ -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 index 0000000..2be03ae --- /dev/null +++ b/interfaces/portal_groups.py @@ -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 index 0000000..aaad7b8 --- /dev/null +++ b/product.txt @@ -0,0 +1 @@ +GroupUserFolder diff --git a/skins/gruf/GroupSpaceFolderishType_view.pt.old b/skins/gruf/GroupSpaceFolderishType_view.pt.old new file mode 100644 index 0000000..79c1267 --- /dev/null +++ b/skins/gruf/GroupSpaceFolderishType_view.pt.old @@ -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 index 0000000..cd3f97f --- /dev/null +++ b/skins/gruf/change_password.py @@ -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 index 0000000000000000000000000000000000000000..eccbeb653185d386b3711a6aae6d7832d283398c GIT binary patch literal 1225 zcmV;)1UCCeNk%w1VM_pD0Oo%HiHV5>1Oz4~CVhQ<At51DR8&$@QZ6noKtMnk7#Ln& zUMeaoK0ZD=Iyz5JPb@4fG&D3aGBO$(8WIu`78Vu`4h{+m3J?$w0s;aY92^V`3<wAa zJv=-V6cqpe|NZ^_{{H@CWMudE_fSw!I5;?FW@acTC^<Pf<KyGv;^Ik3NlQyh($dm! zaByX1Wg{aaOiWB-Vq#fYS#fc3`T6;*tgQ6(^g=>HU0q$Fp`mwocj)NoO-)T!R#s6_ zQSR>U&d$yr9v=Ms{L0G8jEszpjg7Rlv{zSGMn*<QM@PQCzOJsX_4W1U=H?q48`|31 zl9G~WXlQkHb&-*g^78WR?CjCe(SU$}g@uLJ*Vpm!@xj5t`}_N_u&~6$#MIQ(ot>TB z+}z2@$(NUxo12?NL`2KW%hS`-SXfx9s;Y{Liln5ZrlzKunVI3?;gge-wY9bL^Yfsf zpy1%(qN1YN+1bd*$hNk&xVX5vxw*Hux2dVAVPRohTwDME0RR90A^8LW004ggEC2ui z080R1000O7fPaF6goTEOh>41ejE#<ukdcy;l$Dm3n3<ZJoSmMZprN9pq@|{(sHv)} zhAA^A7Z@80XmqTV3mAU8yt@)cw~#{=y~Vo$0Ktu48^+E-%8WtI&I!?qHM-Qr*4Ky~ z+r|M^-i8X{&Sd3<0_esD>x2;Q#Rc($BlNv$_kue5yukGPQv{%&00aLt(MB(TmV_A$ zzDbD3;1Nq6GHT4o=>k9~ls??S66Hh&2@Gm*%w(e(Jpq$E5P86m*M>bSFWCq{4?`G9 zEKJlaW?@XuN@w&DkTfKLI}HOqc(9b?fWBmq>`c(NVa|t2IRGAr<i#mK2O_3Unx_v4 zBo-C_3?5tqV$u?x^bkaHA$J#pua0PJIbi{y2EdO<^z}mkaUv8x2)Hzefxwg<kIc$V zz(vuF7kC%*3ecxfoVa}_P^oLk%~sLWOjud;1GQWYJA@db@ftQY3>+3YS#0444MQe; zyQ|P8QS!bTYKSOQKzUCWhve9%yg>J$6rru78(U}r>=mp9wGa>RMtRr;)CzjQULOWH zgjB$mA^%U|`t(SESRLCLq=S4j0BD~ci1{E|4D$7+AUX+5CrA&AZ7{$fW(+mqHWmzW zg@p8201Q4K0z`-uaw&iq5aFd54k2Rv*HaPcu_dEBg}?w%0Bx|AMT|RAQ^*rGq7#7s z5^5<{B#(qJK_ZAds5RV^=oG>Nk<?UR!&D*^h@~-w5b<LgQ;p%_n7t6<K|}zd=RpH( z&iIE1DI~!qh<SiuRh`tJfI<sX(vtuiEtHYKnryz1<YE&bpnwXR0a_mh72FVl3IZ@{ zsc4uU5Cak(eG1?OFvviFr>6E500b^@uxG1_C4d7D8L&z#Ko*d|M-m$BDqaN+6hMHm z*1cLn1jt%PKnQ;}3+f9YR4T1p7$7jhqu27<1AW~tCIAoN5?5`x*djnDx&|d+z^v>V zvw;l6#M{#i=GjZn0_*BaArk(&bHNet1{?qm#|9h@1WqbUjS~nsyiEryNKB0;20mJB nj0X8=OcV|t7ZX4XA@_no2qp7^K?y1I!g9-gybN=FfdBwIYUm)) literal 0 HcmV?d00001 diff --git a/skins/gruf/folder_localrole_form_plone1.pt b/skins/gruf/folder_localrole_form_plone1.pt new file mode 100644 index 0000000..c4d8e19 --- /dev/null +++ b/skins/gruf/folder_localrole_form_plone1.pt @@ -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> </th> + <th i18n:translate="label_user_group_name">User/Group name</th> + <th i18n:translate="label_type">Type</th> + <th i18n:translate="label_roles">Role(s)</th> + </tr> + </thead> + <tbody> + <tr tal:repeat="lrole python:here.acl_users.getLocalRolesForDisplay(here)"> + <td> + <input class="noborder" + type="checkbox" + name="member_ids:list" + id="#" + value="" + tal:condition="python:lrole[0]!=username" + tal:attributes="value python:lrole[3];" + /> + </td> + + <td tal:content="python:lrole[0]"> + groupname + </td> + + <td tal:condition="python:lrole[2]=='group'" + i18n:translate="label_group"> + Group + </td> + <td tal:condition="python:lrole[2]=='user'" + i18n:translate="label_user"> + User + </td> + + <td> + <tal:block tal:repeat="role python:lrole[1]"> + <span i18n:translate="" + tal:content="role" + tal:omit-tag="">Role</span> + <span tal:condition="not: repeat/role/end" + tal:omit-tag="">, </span> + </tal:block> + </td> + </tr> + </tbody> + </table> + + <input class="context" + type="submit" + value="Delete Selected Role(s)" + i18n:attributes="value" + /> + </form> + + <metal:block tal:condition="python:test(search_submitted and not search_results, 1, 0)"> + <h1 i18n:translate="heading_search_results">Search results</h1> + <p i18n:translate="no_members_found"> + No members were found using your <strong>Search Criteria</strong> + </p> + <hr /> + </metal:block> + + <metal:block tal:condition="python:test(search_submitted and search_results, 1, 0)"> + + <h1 i18n:translate="heading_search_results">Search results</h1> + + <p i18n:translate="description_localrole_select_member"> + Select one or more Members, and a role to assign. + </p> + + <metal:block tal:define="batch python:Batch(search_results, b_size, int(b_start), orphan=3)"> + + <form class="group" + method="post" + name="change_type" + action="folder_localrole_edit" + tal:attributes="action string:${here/absolute_url}/folder_localrole_edit"> + + <span class="legend" i18n:translate="legend_available_members"> + Available Members + </span> + + <input type="hidden" name="change_type" value="add" /> + + <!-- batch navigation --> + <div metal:use-macro="here/batch_macros/macros/navigation" /> + + <table class="listing" summary="Search results"> + <thead> + <tr> + <th> </th> + <th i18n:translate="label_user_name">User Name</th> + <th i18n:translate="label_email_address">Email Address</th> + </tr> + </thead> + <tbody> + <tr tal:repeat="member batch"> + <td> + <input class="noborder" + type="checkbox" + name="member_ids:list" + id="#" + value="" + tal:attributes="value member/username;" + /> + </td> + + <td tal:content="member/username">username</td> + <td tal:content="member/email">email</td> + </tr> + </tbody> + </table> + + <!-- batch navigation --> + <div metal:use-macro="here/batch_macros/macros/navigation" /> + + <div class="row"> + + <div class="label" i18n:translate="label_localrole_to_assign"> + Role to assign + </div> + + <div class="field"> + <select name="member_role"> + <option tal:repeat="lroles python:container.portal_membership.getCandidateLocalRoles(here)" + tal:attributes="value lroles" + tal:content="lroles" + i18n:translate=""> + Role name + </option> + </select> + </div> + + </div> + + <div class="row"> + <div class="label"> </div> + <div class="field"> + <input class="context" + type="submit" + value="Assign Local Role to Selected User(s)" + i18n:attributes="value" + /> + </div> + </div> + + </form> + + </metal:block> + </metal:block> + + <div> + <tal:block tal:condition="python: (not search_submitted or + (search_submitted and not search_results))"> + + <h1 i18n:translate="heading_assign_localrole"> + Assign local roles to folder + <tal:block tal:content="here/title_or_id" i18n:name="folder">title</tal:block> + </h1> + + <p i18n:translate="description_assign_localrole"> + A local role is a way of allowing other users into some or + all of your folders. These users can edit items, publish + them - et cetera, depending on what permissions you give + them. + <br /> + + Local roles are ideal in cooperation projects, and as every + item has a history and an undo option, it's easy to keep + track of the changes. + + <br /> + + To give a person a local role in this folder, just search + for the person's name or email address in the form below, + and you will be presented with a page that will show you the + options available. + </p> + + <form class="group" + method="post" + name="localrole" + action="folder_localrole_form" + tal:attributes="action string:${here/absolute_url}/${template/getId}" > + + <span class="legend" i18n:translate="legend_search_terms"> + Search Terms + </span> + + <input type="hidden" name="role_submit" value="role_submit" /> + + <div class="row"> + <div class="label" i18n:translate="label_search_by"> + Search by + </div> + + <div class="field"> + <select name="search_param"> + <option value="username" i18n:translate="label_user_name"> + User Name + </option> + <option value="email" i18n:translate="label_email_address"> + Email Address + </option> + </select> + </div> + </div> + + <div class="row"> + <div class="label" + i18n:translate="label_search_term"> + Search Term + </div> + + <div class="field"> + <input type="text" + name="search_term" + size="30" + /> + </div> + </div> + + <div class="row"> + <div class="label"> </div> + <div class="field"> + <input class="context" + type="submit" + value="Perform Search" + i18n:attributes="value" + /> + </div> + </div> + + </form> + </tal:block> + + <h1 i18n:translate="heading_available_groups">Available groups</h1> + + <p i18n:translate="description_available_groups"> + Groups are a convenient way to assign roles to a common set of + users. Select one or more Groups, and a role to assign. + </p> + + <form class="group" + method="post" + name="change_type" + action="folder_localrole_edit" + tal:attributes="action string:${here/absolute_url}/folder_localrole_edit"> + + <span class="legend" i18n:translate="legend_available_groups"> + Available Groups + </span> + + <input type="hidden" name="change_type" value="add" /> + + <table class="listing" summary="Available groups"> + <thead> + <tr> + <th> </th> + <th i18n:translate="">Name</th> + </tr> + </thead> + <tbody> + <tr tal:repeat="member here/acl_users/getGroups"> + <td> + <input class="noborder" + type="checkbox" + name="member_ids:list" + id="#" + value="" + tal:attributes="value member/getUserName;" /> + </td> + <td tal:content="python:member.getUserNameWithoutGroupPrefix()"> + groupname + </td> + </tr> + </tbody> + </table> + + <div class="row"> + <div class="label" i18n:translate="label_localrole_to_assign"> + Role to assign + </div> + + <div class="field"> + <select name="member_role"> + <option tal:repeat="lroles python:container.portal_membership.getCandidateLocalRoles(here)" + tal:attributes="value lroles" + tal:content="lroles" + i18n:translate=""> + Role name + </option> + </select> + </div> + </div> + + <div class="row"> + <div class="label"> </div> + <div class="field"> + <input class="context" + type="submit" + value="Assign Local Role to Selected Group(s)" + i18n:attributes="value" + /> + </div> + </div> + + </form> + + </div> + + </div> <!-- fill-slot --> + +</body> +</html> diff --git a/skins/gruf/getUsersInGroup.py b/skins/gruf/getUsersInGroup.py new file mode 100644 index 0000000..358a744 --- /dev/null +++ b/skins/gruf/getUsersInGroup.py @@ -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 index 0000000..0bb76f8 --- /dev/null +++ b/skins/gruf/gruf_ldap_required_fields.py @@ -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 index 0000000..cb9e104 --- /dev/null +++ b/skins/gruf/prefs_group_manage.cpy @@ -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 index 0000000..8bda807 --- /dev/null +++ b/skins/gruf/prefs_group_manage.cpy.metadata @@ -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 index 0000000..2b7785f --- /dev/null +++ b/skins/gruf_plone_2_0/README.txt @@ -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 index 0000000..ed0d362 --- /dev/null +++ b/skins/gruf_plone_2_0/folder_localrole_form.pt @@ -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 index 0000000..5a8a033 --- /dev/null +++ b/svn-commit.tmp @@ -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 index 0000000000000000000000000000000000000000..8aa90b53b65ee410b8a9be3819fcba223c936c4e GIT binary patch literal 166 zcmZ?wbhEHb6krfwc+9}CY0HL=j*jye&gSIiPn<a6=+R>pRTa;lKc6*c#>$l|!@|OE z+_;gLl<4i_^&bor|MB@IrlcyAXO?6rxO@5rFev_HVdP@qXV75)0+6W;ERG*edamB8 z5Tng@Gc-|#-=T@ALFE+FH0H}Z`3En&SC;VkC&SXpufZbA$LjKNlL8A@=P~XD7D8Nh Qa~U7Jeo%8GoQ1&}0ANHx<p2Nx literal 0 HcmV?d00001 diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..c5ddf26 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +3.55.1 diff --git a/www/GRUFGroups.gif b/www/GRUFGroups.gif new file mode 100644 index 0000000000000000000000000000000000000000..6a7fb9f7854132370967600d39d87e4ad0207ff4 GIT binary patch literal 607 zcmZwEJ7^R^0LJmn!{jhQR}*0p6|*Afm`Jkj`B)!FCYr>Fidn_S7zknX)LTr%K{?n) zHWMLan!q-Jt&~Z3U^AfLRl*h_Zn<Eyg@|W&U~^cFm)>9dHGlY~u1}tK7fi!6ei=rg zP~bVQc>tgU2moxsfSE}mBT85kGFWfw?G1-iDMBbytR?nMR|G`lLI{`C2_s0MW@M)V zvKmKJ#+kM)GpT1111;S=i31<0JV|1YtA?#?VVe*_OdG`4v_`e^R5e1La2zM47V&63 zfLdz{ryf(1qf`l(xl&43xX90aq4UW05|8o>;#M^9WT2~<b6%IoCW#W*DLE`-v>G(B zYNog(R;gvX)NDms77es%npk4J;UEZzV%U=|w{eg+B8V}DIJTLsYaYZ&;#-I`(ioGV zkt@H7O+rXk6TmE_zEvZ@LW!=C$TqdYAObLegi%bJgz37(fn`CQrm<)IkM*~+v)|Ch z$j-R)iP8P#trITum3J%Fvm5959<%@I=yLaynZ0M1pWbrU=bU4`w0B@%I68N)d>Sk` zujgOPk6hk7aT9>sW#`(`$kN0>IDXhKm&Zy@W$4?`(eTdmBfj5$Jp2S6@o{5uvCF<Y zd}^xjkRRMN`{u#q{kEPSJbWR%;$7-H)c5!RnygI!`Ptq!So}TxeHP7xyZ0B1l^<U_ p<F<~I+q3KMLooE}Wo7k)vAQyNrt9P9j?J~Ti7~oW>@OG>{{e%N?3Vxl literal 0 HcmV?d00001 diff --git a/www/GRUFUsers.gif b/www/GRUFUsers.gif new file mode 100644 index 0000000000000000000000000000000000000000..cc199e6cb39bbf81e924769c2ca2cb8691e12b7c GIT binary patch literal 539 zcmdUsJx|*}06;HkT0+x;TM}@;5)RT9Lr6IiNUllvI3y4^-slZQ%5tNZN?uBEGI{iZ z<cv4oQprPyW5W|-M4dX;W2V;XbX9&v-w$|t(mOjpZQ~0A7~mHG7NJv}CO#A#B8kk< z{3&h2HA1i7SAA}y2g#KSlaNLMWei&Zvbp0sm?%OW)46xj5GTsfl2XBx;L^jG(^U4g zAX;nbQ&;)Zh!SR-4%CV<ZZXBI&`Ol=a@4qPNY|PQGPoPW0gYW#M@(Ngpnz0SEO8n! zBv9g8Dqzwlu?NM>V;i0kI9NwC7M#T_6$Jc;z`wa%5ol1Gwa=b_xi4QU!z*tzdcClk zACI~PYvswNKg@SO72Z966jbnbueZOp|1vl}IYRG|c{u5mzm=DtPHMZ~ciW4T%1*YO z{cuw$zIpa}erwV!y&Bd_cjJv_y`IVZXuTaba!s-YKQ2|l_~3DCZ*R`1uD`hb{pS*N F{sB2>h*<yt literal 0 HcmV?d00001 diff --git a/www/GroupUserFolder.gif b/www/GroupUserFolder.gif new file mode 100644 index 0000000000000000000000000000000000000000..cbfdef2b52bfb39e01d14e9e3706eb3a131b2ba6 GIT binary patch literal 600 zcmZ?wbhEHb6krfwc*Xz%m+!L8@DLVtIQsbYSD*02X$w}YezZeXTfct)*S9Ne3-1)A zrKK(Fws(yEBs@*npu^zE()<gViai(g8mb!1TP=K+1RUBl*CKM!*EYop7n%S68J^xg zap1rK74Pd6M<(ZM8?N5m<kJ><txqd;{`-f^y$+u9o0(>;C(ia@XZijrJFnwjAMOe% zIZd9oDZ0L@;Q#;s>o=~>D=_zSbI5T_y19R<S<Ieo0UZB#285No2v}d3URd+;<;(s3 zk!r5HP2vt+>-GLW)A;je1|ELC`_+0gjTv&<{`(bdS+rznKtModU8Yy@v)3DZkL=tT zoc-(a^4NRN7`}Y@@_&lf?P=COH)thpD>Y8KV&Hu<-^OTHjsK&^d>dcvU(;e?ImyzY z-oY+t=7%R2+H3Q}3%<R7|9<AonOnDRy>p<ytg3pxHFrZp!@YYPG7e{N<jMcv9r)uL z*Ru7?zO3^MD*pE932RGjAvkmi5Q;xpSVS29Gw6WSf#QULeQSe3Q*%pei)V*tTdSw6 zbYEAuL2v)m&go5_(guMx$u><1X{Is;43iC{Q+U`|t@8x^HDmObcNrQPM{22ytNL&$ zCvG#y;*gRIa?vTs%?&O-;^EAcBV_Iy;^=6be#FUBBO@b1SkZx(G4qJ5ny!M0y;U?z zsF>FgJxezg&cZNxKAnh<4L2D0{f_7qHaNPp>Iv)-uue)oD=Htq<w(OPH$K*A&r2OL Uo<UDE9T|dF#mp3PQ(&+L0J?wnkN^Mx literal 0 HcmV?d00001 diff --git a/www/LDAPGroupFolder.gif b/www/LDAPGroupFolder.gif new file mode 100644 index 0000000000000000000000000000000000000000..cbb71a41b288e439c4f10506f8c22a790905d8ae GIT binary patch literal 977 zcmZ?wbhEHb6krfw_|Cu(?P(O{X%gdM=;!B`J6*o4Bcx{GiODJ26VnW)rQ7bVGHEN0 zX)lSJTHrXP*nL)YNN-*4vU1;>by|1pb(*)Sw(r#L+GE@?H*r#9&ZPYgyXKZoKkPp1 zsPFvaVJpvOZoW`=Xlm4>sm{+Ahdo~!_I7K|hyB&_r}wnX-a2{tzQv0duV24@>Y<O* z4}F|>?9<W{Zx){Xy!`a%wdcQVKL38-&2I-+cAVcj;l_^6LpNufdbIfZ^SzIcO#gVe z`O~Ss@3&U}e6Z{8%`3<5em{Qi`>6-t&prBi@yU<NPk&y2@$=TpU%y|SfB5j>t5>i7 z|NqZ03PwW!M+hkXWMO1r@Mh2f$%FC)1IIds=^Qd18x|aFv^UT>@Zqt-Ayz&q1%V3_ z5)QT6%U!v^_=tg_TR@d-!NbMJ`B)h_j4qtGsN654q{5@X;plvFGNW6|j0p;yj>2Yf z$7BQ+uJCX23VQKmWfRLJA!ari(G^QFT4XG^rtH{ou<44bXRt-9)q`WLe3DuL5)-y` zOb}KLI8b2tfSH}2<=Qd}CzTUCB8D+r5(Kw?Xb^F0+ryxA*p-J_J59*M$8y!-(=5zP G4AuZvXxKdf literal 0 HcmV?d00001 diff --git a/www/down_arrow.gif b/www/down_arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..f8da0c6efdee8dca2198c19f00678f1f21d0cc09 GIT binary patch literal 56 zcmZ?wbhEHb<Y3@nXkcLY|NlQj+DyftEUY373=BFz3>4>JU|`~J5nk3%{qmNxN$AOp JmIVw9)&P@L4-fzV literal 0 HcmV?d00001 diff --git a/www/down_arrow_grey.gif b/www/down_arrow_grey.gif new file mode 100644 index 0000000000000000000000000000000000000000..5e90141178cbca615b27921ff0bf5249af91fb7d GIT binary patch literal 56 zcmZ?wbhEHb<Y3@nXkcLY|NsA)GiMZkvapIUFfix<F;JX?fq{v?MR-|5^~+n%CZQ)Y KS{5)cSOWmcQ4n<i literal 0 HcmV?d00001 diff --git a/www/up_arrow.gif b/www/up_arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..7ed42e2e0cc11bbbfb415fd26363e59de4f4e1bb GIT binary patch literal 54 zcmZ?wbhEHb<Y3@nXkcJqNSpcp|9{1wEUY37j0`$J0iZYs0|OIp596|q(|_j$Pi{*q I)L^g%0CC0+e*gdg literal 0 HcmV?d00001 diff --git a/www/up_arrow_grey.gif b/www/up_arrow_grey.gif new file mode 100644 index 0000000000000000000000000000000000000000..7679aa43a0a9e4ed80a94f5206f46286862d12f9 GIT binary patch literal 54 zcmZ?wbhEHb<Y3@nXkcJCbLPzd|Nj+#vapIUFf!;c00Bsbfr+<=aaqUdzw?48x1|+o HFjxZsswWTR literal 0 HcmV?d00001 -- 2.20.1