Back to index

plone3  3.1.7
sharing.py
Go to the documentation of this file.
00001 from itertools import chain
00002 
00003 from zope.component import getUtilitiesFor, getMultiAdapter
00004 
00005 from Products.Five.browser import BrowserView
00006 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
00007 
00008 from Acquisition import aq_inner, aq_parent, aq_base
00009 from AccessControl import Unauthorized
00010 from zExceptions import Forbidden
00011 
00012 from Products.CMFCore.utils import getToolByName
00013 from Products.CMFCore import permissions
00014 from Products.CMFPlone import PloneMessageFactory as _
00015 from Products.statusmessages.interfaces import IStatusMessage
00016 
00017 from plone.memoize.instance import memoize, clearafter
00018 
00019 from plone.app.workflow.interfaces import ISharingPageRole
00020 
00021 AUTH_GROUP = 'AuthenticatedUsers'
00022 STICKY = (AUTH_GROUP,)
00023 
00024 def merge_search_results(results, key):
00025     """Merge member search results.
00026 
00027     Based on PlonePAS.browser.search.PASSearchView.merge.
00028     """
00029     output={}
00030     for entry in results:
00031         id=entry[key]
00032         if id not in output:
00033             output[id]=entry.copy()
00034         else:
00035             buf=entry.copy()
00036             buf.update(output[id])
00037             output[id]=buf
00038 
00039     return output.values()
00040 
00041 class SharingView(BrowserView):
00042     
00043     # Actions
00044     
00045     template = ViewPageTemplateFile('sharing.pt')
00046     
00047     def __call__(self):
00048         """Perform the update and redirect if necessary, or render the page
00049         """
00050         
00051         postback = True
00052         
00053         form = self.request.form
00054         submitted = form.get('form.submitted', False)
00055     
00056         save_button = form.get('form.button.Save', None) is not None
00057         cancel_button = form.get('form.button.Cancel', None) is not None
00058     
00059         if submitted and not cancel_button:
00060 
00061             if not self.request.get('REQUEST_METHOD','GET') == 'POST':
00062                 raise Forbidden
00063 
00064             authenticator = self.context.restrictedTraverse('@@authenticator', None) 
00065             if not authenticator.verify(): 
00066                 raise Forbidden
00067 
00068             # Update the acquire-roles setting
00069             inherit = bool(form.get('inherit', False))
00070             reindex = self.update_inherit(inherit, reindex=False)
00071 
00072             # Update settings for users and groups
00073             entries = form.get('entries', [])
00074             roles = [r['id'] for r in self.roles()]
00075             settings = []
00076             for entry in entries:
00077                 settings.append(
00078                     dict(id = entry['id'],
00079                          type = entry['type'],
00080                          roles = [r for r in roles if entry.get('role_%s' % r, False)]))
00081             if settings:
00082                 reindex = self.update_role_settings(settings, reindex=False) or reindex
00083                 
00084             if reindex:
00085                 aq_inner(self.context).reindexObjectSecurity()
00086             IStatusMessage(self.request).addStatusMessage(_(u"Changes saved."), type='info')
00087             
00088         # Other buttons return to the sharing page
00089         if cancel_button:
00090             postback = False
00091         
00092         if postback:
00093             return self.template()
00094         else:
00095             context_state = self.context.restrictedTraverse("@@plone_context_state")
00096             url = context_state.view_url()
00097             self.request.response.redirect(url)
00098             
00099     # View
00100     
00101     @memoize
00102     def roles(self):
00103         """Get a list of roles that can be managed.
00104         
00105         Returns a list of dicts with keys:
00106         
00107             - id
00108             - title
00109         """
00110         context = aq_inner(self.context)
00111         portal_membership = getToolByName(context, 'portal_membership')
00112         
00113         pairs = []
00114         
00115         for name, utility in getUtilitiesFor(ISharingPageRole):
00116             permission = utility.required_permission
00117             if permission is None or portal_membership.checkPermission(permission, context):
00118                 pairs.append(dict(id = name, title = utility.title))
00119                 
00120         pairs.sort(key=lambda x: x["id"])
00121         return pairs
00122         
00123     @memoize
00124     def role_settings(self):
00125         """Get current settings for users and groups for which settings have been made.
00126         
00127         Returns a list of dicts with keys:
00128         
00129          - id
00130          - title
00131          - type (one of 'group' or 'user')
00132          - roles
00133          
00134         'roles' is a dict of settings, with keys of role ids as returned by 
00135         roles(), and values True if the role is explicitly set, False
00136         if the role is explicitly disabled and None if the role is inherited.
00137         """
00138         
00139         existing_settings = self.existing_role_settings()
00140         user_results = self.user_search_results()
00141         group_results = self.group_search_results()
00142 
00143         current_settings = existing_settings + user_results + group_results
00144 
00145         # We may be called when the user does a search instead of an update.
00146         # In that case we must not loose the changes the user made and
00147         # merge those into the role settings.
00148         requested = self.request.form.get('entries', None)
00149         if requested is not None:
00150             knownroles = [r['id'] for r in self.roles()]
00151             settings = {}
00152             for entry in requested:
00153                 roles = [r for r in knownroles
00154                                 if entry.get('role_%s' % r, False)]
00155                 settings[(entry['id'], entry['type'])] = roles
00156 
00157             for entry in current_settings:
00158                 desired_roles = settings.get((entry['id'], entry['type']), None)
00159 
00160                 if desired_roles is None:
00161                     continue
00162                 for role in entry["roles"]:
00163                     if entry["roles"][role] in [True, False]:
00164                         entry["roles"][role] = role in desired_roles
00165 
00166         current_settings.sort(key=lambda x: x["type"]+str(x["title"]))
00167 
00168         return current_settings
00169 
00170     def inherited(self, context=None):
00171         """Return True if local roles are inherited here.
00172         """
00173         if context is None:
00174             context = self.context
00175         if getattr(aq_base(context), '__ac_local_roles_block__', None):
00176             return False
00177         return True
00178         
00179     # helper functions
00180     
00181     @memoize
00182     def existing_role_settings(self):
00183         """Get current settings for users and groups that have already got
00184         at least one of the managed local roles.
00185 
00186         Returns a list of dicts as per role_settings()
00187         """
00188         context = aq_inner(self.context)
00189         
00190         portal_membership = getToolByName(context, 'portal_membership')
00191         portal_groups = getToolByName(context, 'portal_groups')
00192         acl_users = getToolByName(context, 'acl_users')
00193         
00194         info = []
00195         
00196         # This logic is adapted from computeRoleMap.py
00197         
00198         local_roles = acl_users._getLocalRolesForDisplay(context)
00199         acquired_roles = self._inherited_roles()
00200         available_roles = [r['id'] for r in self.roles()]
00201 
00202         # first process acquired roles
00203         items = {}
00204         for name, roles, rtype, rid in acquired_roles:
00205             items[rid] = dict(id       = rid,
00206                               name     = name,
00207                               type     = rtype,
00208                               sitewide = [],
00209                               acquired = roles,
00210                               local    = [],)
00211                                 
00212         # second process local roles
00213         for name, roles, rtype, rid in local_roles:
00214             if items.has_key(rid):
00215                 items[rid]['local'] = roles
00216             else:
00217                 items[rid] = dict(id       = rid,
00218                                   name     = name,
00219                                   type     = rtype,
00220                                   sitewide = [],
00221                                   acquired = [],
00222                                   local    = roles,)
00223 
00224         # Make sure we always get the authenticated users virtual group
00225         if AUTH_GROUP not in items:
00226             items[AUTH_GROUP] = dict(id = AUTH_GROUP,
00227                                      name = _(u'Logged-in users'),
00228                                      type  = 'group',
00229                                      sitewide = [],
00230                                      acquired = [],
00231                                      local = [],)
00232 
00233         # If the current user has been given roles, remove them so that he
00234         # doesn't accidentally lock himself out.
00235         
00236         member = portal_membership.getAuthenticatedMember()
00237         if member.getId() in items:
00238             items[member.getId()]['disabled'] = True
00239 
00240         # Sort the list: first the authenticated users virtual group, then 
00241         # all other groups and then all users, alphabetically
00242 
00243         dec_users = [( a['id'] not in STICKY,
00244                        a['type'], 
00245                        a['name'],
00246                        a) for a in items.values()]
00247         dec_users.sort()
00248         
00249         # Add the items to the info dict, assigning full name if possible.
00250         # Also, recut roles in the format specified in the docstring
00251         
00252         for d in dec_users:
00253             item = d[-1]
00254             name = item['name']
00255             rid = item['id']
00256             global_roles = set()
00257             
00258             if item['type'] == 'user':
00259                 member = acl_users.getUserById(rid)
00260                 if member is not None:
00261                     name = member.getProperty('fullname') or member.getId() or name
00262                     global_roles = set(member.getRoles())
00263             elif item['type'] == 'group':
00264                 g = portal_groups.getGroupById(rid)
00265                 name = g.getGroupTitleOrName()
00266                 global_roles = set(g.getRoles())
00267                 
00268                 # This isn't a proper group, so it needs special treatment :(
00269                 if rid == AUTH_GROUP:
00270                     name = _(u'Logged-in users')
00271             
00272             info_item = dict(id    = item['id'],
00273                              type  = item['type'],
00274                              title = name,
00275                              disabled = item.get('disabled', False),
00276                              roles = {})
00277                              
00278             # Record role settings
00279             have_roles = False
00280             for r in available_roles:
00281                 if r in global_roles:
00282                     info_item['roles'][r] = 'global'
00283                 elif r in item['acquired']:
00284                     info_item['roles'][r] = 'acquired'
00285                     have_roles = True # we want to show acquired roles
00286                 elif r in item['local']:
00287                     info_item['roles'][r] = True
00288                     have_roles = True # at least one role is set
00289                 else:
00290                     info_item['roles'][r] = False
00291                     
00292             if have_roles or rid in STICKY:
00293                 info.append(info_item)
00294             
00295         return info
00296     
00297     def _principal_search_results(self,
00298                                   search_for_principal,
00299                                   get_principal_by_id,
00300                                   get_principal_title,
00301                                   principal_type,
00302                                   id_key):
00303         """Return search results for a query to add new users or groups.
00304         
00305         Returns a list of dicts, as per role_settings().
00306         
00307         Arguments:
00308             search_for_principal -- a function that takes an IPASSearchView and
00309                 a search string. Uses the former to search for the latter and
00310                 returns the results.
00311             get_principal_by_id -- a function that takes a user id and returns
00312                 the user of that id
00313             get_principal_title -- a function that takes a user and a default
00314                 title and returns a human-readable title for the user. If it
00315                 can't think of anything good, returns the default title.
00316             principal_type -- either 'user' or 'group', depending on what kind
00317                 of principals you want
00318             id_key -- the key under which the principal id is stored in the
00319                 dicts returned from search_for_principal
00320         """
00321         context = aq_inner(self.context)
00322         
00323         search_term = self.request.form.get('search_term', None)
00324         if not search_term:
00325             return []
00326         
00327         existing_principals = set([p['id'] for p in self.existing_role_settings() 
00328                                 if p['type'] == principal_type])
00329         empty_roles = dict([(r['id'], False) for r in self.roles()])
00330         info = []
00331         
00332         hunter = getMultiAdapter((context, self.request), name='pas_search')
00333         for principal_info in search_for_principal(hunter, search_term):
00334             principal_id = principal_info[id_key]
00335             if principal_id not in existing_principals:
00336                 principal = get_principal_by_id(principal_id)
00337                 roles = empty_roles.copy()
00338                 if principal is None:
00339                     continue
00340 
00341                 for r in principal.getRoles():
00342                     if r in roles:
00343                         roles[r] = 'global'
00344                 info.append(dict(id    = principal_id,
00345                                  title = get_principal_title(principal,
00346                                                              principal_id),
00347                                  type  = principal_type,
00348                                  roles = roles))
00349         return info
00350         
00351     def user_search_results(self):
00352         """Return search results for a query to add new users.
00353         
00354         Returns a list of dicts, as per role_settings().
00355         """
00356         def search_for_principal(hunter, search_term):
00357             return merge_search_results(chain(*[hunter.searchUsers(**{field: search_term})
00358                                  for field in ['login', 'fullname']]
00359                               ), 'userid')
00360         
00361         def get_principal_by_id(user_id):
00362             acl_users = getToolByName(self.context, 'acl_users')
00363             return acl_users.getUserById(user_id)
00364         
00365         def get_principal_title(user, default_title):
00366             return user.getProperty('fullname') or user.getId() or default_title
00367             
00368         return self._principal_search_results(search_for_principal,
00369             get_principal_by_id, get_principal_title, 'user', 'userid')
00370         
00371     def group_search_results(self):
00372         """Return search results for a query to add new groups.
00373         
00374         Returns a list of dicts, as per role_settings().
00375         """
00376         def search_for_principal(hunter, search_term):
00377             return hunter.searchGroups(id=search_term)
00378         
00379         def get_principal_by_id(group_id):
00380             portal_groups = getToolByName(self.context, 'portal_groups')
00381             return portal_groups.getGroupById(group_id)
00382         
00383         def get_principal_title(group, _):
00384             return group.getGroupTitleOrName()
00385             
00386         return self._principal_search_results(search_for_principal,
00387             get_principal_by_id, get_principal_title, 'group', 'groupid')
00388         
00389     def _inherited_roles(self):
00390         """Returns a tuple with the acquired local roles."""
00391         context = aq_inner(self.context)
00392         
00393         if not self.inherited(context):
00394             return []
00395         
00396         portal = getToolByName(context, 'portal_url').getPortalObject()
00397         result = []
00398         cont = True
00399         if portal != context:
00400             parent = aq_parent(context)
00401             while cont:
00402                 if not getattr(parent, 'acl_users', False):
00403                     break
00404                 userroles = parent.acl_users._getLocalRolesForDisplay(parent)
00405                 for user, roles, role_type, name in userroles:
00406                     # Find user in result
00407                     found = 0
00408                     for user2, roles2, type2, name2 in result:
00409                         if user2 == user:
00410                             # Check which roles must be added to roles2
00411                             for role in roles:
00412                                 if not role in roles2:
00413                                     roles2.append(role)
00414                             found = 1
00415                             break
00416                     if found == 0:
00417                         # Add it to result and make sure roles is a list so
00418                         # we may append and not overwrite the loop variable
00419                         result.append([user, list(roles), role_type, name])
00420                 if parent == portal:
00421                     cont = False
00422                 elif not self.inherited(parent):
00423                     # Role acquired check here
00424                     cont = False
00425                 else:
00426                     parent = aq_parent(parent)
00427 
00428         # Tuplize all inner roles
00429         for pos in range(len(result)-1,-1,-1):
00430             result[pos][1] = tuple(result[pos][1])
00431             result[pos] = tuple(result[pos])
00432 
00433         return tuple(result)
00434         
00435     def update_inherit(self, status=True, reindex=True):
00436         """Enable or disable local role acquisition on the context.
00437 
00438         Returns True if changes were made, or False if the new settings
00439         are the same as the existing settings.
00440         """
00441         context = aq_inner(self.context)
00442         portal_membership = getToolByName(context, 'portal_membership')
00443         
00444         if not portal_membership.checkPermission(permissions.ModifyPortalContent, context):
00445             raise Unauthorized
00446 
00447         block = not status 
00448         oldblock = bool(getattr(aq_base(context), '__ac_local_roles_block__', False))
00449 
00450         if block == oldblock:
00451             return False
00452 
00453         context.__ac_local_roles_block__ = block and True or None
00454         if reindex:
00455             context.reindexObjectSecurity()
00456         return True
00457         
00458     @clearafter
00459     def update_role_settings(self, new_settings, reindex=True):
00460         """Update local role settings and reindex object security if necessary.
00461         
00462         new_settings is a list of dicts with keys id, for the user/group id;
00463         type, being either 'user' or 'group'; and roles, containing the list
00464         of role ids that are set.
00465 
00466         Returns True if changes were made, or False if the new settings
00467         are the same as the existing settings.
00468         """
00469 
00470         changed = False
00471         context = aq_inner(self.context)
00472             
00473         managed_roles = frozenset([r['id'] for r in self.roles()])
00474         member_ids_to_clear = []
00475             
00476         for s in new_settings:
00477             user_id = s['id']
00478             
00479             existing_roles = frozenset(context.get_local_roles_for_userid(userid=user_id))
00480             selected_roles = frozenset(s['roles'])
00481 
00482             relevant_existing_roles = managed_roles & existing_roles
00483 
00484             # If, for the managed roles, the new set is the same as the
00485             # current set we do not need to do anything.
00486             if relevant_existing_roles == selected_roles:
00487                 continue
00488             
00489             # We will remove those roles that we are managing and which set
00490             # on the context, but which were not selected
00491             to_remove = relevant_existing_roles - selected_roles
00492 
00493             # Leaving us with the selected roles, less any roles that we
00494             # want to remove
00495             wanted_roles = (selected_roles | existing_roles) - to_remove
00496             
00497             # take away roles that we are managing, that were not selected 
00498             # and which were part of the existing roles
00499             
00500             if wanted_roles:
00501                 context.manage_setLocalRoles(user_id, list(wanted_roles))
00502                 changed = True
00503             elif existing_roles:
00504                 member_ids_to_clear.append(user_id)
00505                 
00506         if member_ids_to_clear:
00507             context.manage_delLocalRoles(userids=member_ids_to_clear)
00508             changed = True
00509         
00510         if changed and reindex:
00511             self.context.reindexObjectSecurity()
00512 
00513         return changed