import re import sys from types import UnicodeType, StringType import urlparse from Products.CMFPlone.utils import log from Products.CMFPlone.utils import log_exc import transaction from Products.CMFPlone import utils from Acquisition import aq_base, aq_inner, aq_parent from ComputedAttribute import ComputedAttribute from Products.CMFCore.utils import UniqueObject from Products.CMFCore.utils import getToolByName from Products.CMFCore import permissions from Products.CMFCore.permissions import AccessContentsInformation, \ ManagePortal, ManageUsers, ModifyPortalContent, View from Products.CMFCore.interfaces.DublinCore import DublinCore, MutableDublinCore from Products.CMFCore.interfaces.Discussions import Discussable from Products.CMFCore.WorkflowCore import WorkflowException from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl from Products.CMFPlone.interfaces.Translatable import ITranslatable from Products.CMFPlone.interfaces import INonStructuralFolder from Products.CMFPlone import ToolNames from Products.CMFPlone.utils import transaction_note from Products.CMFPlone.utils import base_hasattr from Products.CMFPlone.utils import safe_hasattr from Products.CMFPlone.utils import postonly from Products.CMFPlone.interfaces import IBrowserDefault from OFS.SimpleItem import SimpleItem from OFS.ObjectManager import bad_id from Globals import InitializeClass from AccessControl import ClassSecurityInfo, Unauthorized from ZODB.POSException import ConflictError from Products.CMFPlone.PloneBaseTool import PloneBaseTool from DateTime import DateTime DateTime.SyntaxError from Products.CMFPlone.PloneFolder import ReplaceableWrapper from Products.statusmessages.interfaces import IStatusMessage AllowSendto = 'Allow sendto' permissions.setDefaultRoles(AllowSendto, ('Anonymous', 'Manager',)) _marker = utils._marker _icons = {} CEILING_DATE = DefaultDublinCoreImpl._DefaultDublinCoreImpl__CEILING_DATE FLOOR_DATE = DefaultDublinCoreImpl._DefaultDublinCoreImpl__FLOOR_DATE from Products.SecureMailHost.SecureMailHost import EMAIL_RE from Products.SecureMailHost.SecureMailHost import EMAIL_CUTOFF_RE BAD_CHARS = re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# ]').findall # XXX Remove this when we don't depend on python2.1 any longer, # use email.Utils.getaddresses instead from rfc822 import AddressList def _getaddresses(fieldvalues): """Return a list of (REALNAME, EMAIL) for each fieldvalue.""" all = ', '.join(fieldvalues) a = AddressList(all) return a.addresslist # dublic core accessor name -> metadata name METADATA_DCNAME = { # The first two rows are handle in a special way # 'Description' : 'description', # 'Subject' : 'keywords', 'Description' : 'DC.description', 'Subject' : 'DC.subject', 'Creator' : 'DC.creator', 'Contributors' : 'DC.contributors', 'Publisher' : 'DC.publisher', 'CreationDate' : 'DC.date.created', 'ModificationDate' : 'DC.date.modified', 'Type' : 'DC.type', 'Format' : 'DC.format', 'Language' : 'DC.language', 'Rights' : 'DC.rights', } class PloneTool(PloneBaseTool, UniqueObject, SimpleItem): """Various utility methods.""" id = 'plone_utils' meta_type = ToolNames.UtilsTool toolicon = 'skins/plone_images/site_icon.gif' security = ClassSecurityInfo() plone_tool = 1 # Prefix for forms fields!? field_prefix = 'field_' __implements__ = (PloneBaseTool.__implements__, SimpleItem.__implements__, ) security.declareProtected(ManageUsers, 'setMemberProperties') def setMemberProperties(self, member, **properties): membership = getToolByName(self, 'portal_membership') if safe_hasattr(member, 'getId'): member = member.getId() user = membership.getMemberById(member) user.setMemberProperties(properties) security.declarePublic('getSiteEncoding') def getSiteEncoding(self): """ Get the default_charset or fallback to utf8. >>> ptool = self.portal.plone_utils >>> ptool.getSiteEncoding() 'utf-8' """ return utils.getSiteEncoding(self) security.declarePublic('portal_utf8') def portal_utf8(self, str, errors='strict'): """ Transforms an string in portal encoding to utf8. >>> ptool = self.portal.plone_utils >>> text = u'Eksempel \xe6\xf8\xe5' >>> sitetext = text.encode(ptool.getSiteEncoding()) >>> ptool.portal_utf8(sitetext) == text.encode('utf-8') True """ return utils.portal_utf8(self, str, errors) security.declarePublic('utf8_portal') def utf8_portal(self, str, errors='strict'): """ Transforms an utf8 string to portal encoding. >>> ptool = self.portal.plone_utils >>> text = u'Eksempel \xe6\xf8\xe5' >>> utf8text = text.encode('utf-8') >>> ptool.utf8_portal(utf8text) == text.encode(ptool.getSiteEncoding()) True """ return utils.utf8_portal(self, str, errors) security.declarePrivate('getMailHost') def getMailHost(self): """ Gets the MailHost. >>> ptool = self.portal.plone_utils >>> ptool.getMailHost() """ return getattr(aq_parent(self), 'MailHost') security.declareProtected(AllowSendto, 'sendto') def sendto(self, send_to_address, send_from_address, comment, subject='Plone', **kwargs): """Sends a link of a page to someone.""" host = self.getMailHost() template = getattr(self, 'sendto_template') encoding = self.getSiteEncoding() if 'envelope_from' in kwargs: envelope_from = kwargs['envelope_from'] else: envelope_from = send_from_address # Cook from template message = template(self, send_to_address=send_to_address, send_from_address=send_from_address, comment=comment, subject=subject, **kwargs) result = host.secureSend(message, send_to_address, envelope_from, subject=subject, subtype='plain', charset=encoding, debug=False, From=send_from_address) security.declarePublic('validateSingleNormalizedEmailAddress') def validateSingleNormalizedEmailAddress(self, address): """Lower-level function to validate a single normalized email address, see validateEmailAddress. """ host = self.getMailHost() return host.validateSingleNormalizedEmailAddress(address) security.declarePublic('validateSingleEmailAddress') def validateSingleEmailAddress(self, address): """Validate a single email address, see also validateEmailAddresses.""" host = self.getMailHost() return host.validateSingleEmailAddress(address) security.declarePublic('validateEmailAddresses') def validateEmailAddresses(self, addresses): """Validate a list of possibly several email addresses, see also validateSingleEmailAddress. """ host = self.getMailHost() return host.validateEmailAddresses(addresses) security.declarePublic('editMetadata') def editMetadata(self , obj , allowDiscussion=None , title=None , subject=None , description=None , contributors=None , effective_date=None , expiration_date=None , format=None , language=None , rights=None , **kwargs): """Responsible for setting metadata on a content object. We assume the obj implements IDublinCoreMetadata. """ mt = getToolByName(self, 'portal_membership') if not mt.checkPermission(ModifyPortalContent, obj): # FIXME: Some scripts rely on this being string? raise Unauthorized REQUEST = self.REQUEST pfx = self.field_prefix def getfield(request, name, default=None, pfx=pfx): return request.form.get(pfx + name, default) def tuplify(value): return tuple(filter(None, value)) if DublinCore.isImplementedBy(obj): if title is None: title = getfield(REQUEST, 'title') if description is None: description = getfield(REQUEST, 'description') if subject is None: subject = getfield(REQUEST, 'subject') if subject is not None: subject = tuplify(subject) if contributors is None: contributors = getfield(REQUEST, 'contributors') if contributors is not None: contributors = tuplify(contributors) if effective_date is None: effective_date = getfield(REQUEST, 'effective_date') if effective_date == '': effective_date = 'None' if expiration_date is None: expiration_date = getfield(REQUEST, 'expiration_date') if expiration_date == '': expiration_date = 'None' if Discussable.isImplementedBy(obj) or \ getattr(obj, '_isDiscussable', None): disc_tool = getToolByName(self, 'portal_discussion') if allowDiscussion is None: allowDiscussion = disc_tool.isDiscussionAllowedFor(obj) if not safe_hasattr(obj, 'allow_discussion'): allowDiscussion = None allowDiscussion = REQUEST.get('allowDiscussion', allowDiscussion) if type(allowDiscussion) == StringType: allowDiscussion = allowDiscussion.lower().strip() if allowDiscussion == 'default': allowDiscussion = None elif allowDiscussion == 'off': allowDiscussion = 0 elif allowDiscussion == 'on': allowDiscussion = 1 disc_tool.overrideDiscussionFor(obj, allowDiscussion) if MutableDublinCore.isImplementedBy(obj): if title is not None: obj.setTitle(title) if description is not None: obj.setDescription(description) if subject is not None: obj.setSubject(subject) if contributors is not None: obj.setContributors(contributors) if effective_date is not None: obj.setEffectiveDate(effective_date) if expiration_date is not None: obj.setExpirationDate(expiration_date) if format is not None: obj.setFormat(format) if language is not None: obj.setLanguage(language) if rights is not None: obj.setRights(rights) # Make the catalog aware of changes obj.reindexObject() def _renameObject(self, obj, id): if not id: REQUEST = self.REQUEST id = REQUEST.get('id', '') id = REQUEST.get(self.field_prefix + 'id', '') if id != obj.getId(): parent = aq_parent(aq_inner(obj)) parent.manage_renameObject(obj.getId(), id) def _makeTransactionNote(self, obj, msg=''): #TODO Why not aq_parent()? relative_path = '/'.join(getToolByName(self, 'portal_url').getRelativeContentPath(obj)[:-1]) charset = self.getSiteEncoding() if not msg: msg = relative_path + '/' + obj.title_or_id() + ' has been modified.' if isinstance(msg, UnicodeType): # Convert unicode to a regular string for the backend write IO. # UTF-8 is the only reasonable choice, as using unicode means # that Latin-1 is probably not enough. msg = msg.encode(charset) if not transaction.get().description: transaction_note(msg) security.declarePublic('contentEdit') def contentEdit(self, obj, **kwargs): """Encapsulates how the editing of content occurs.""" try: self.editMetadata(obj, **kwargs) except AttributeError, msg: log('Failure editing metadata at: %s.\n%s\n' % (obj.absolute_url(), msg)) if kwargs.get('id', None) is not None: self._renameObject(obj, id=kwargs['id'].strip()) self._makeTransactionNote(obj) security.declarePublic('availableMIMETypes') def availableMIMETypes(self): """Returns a map of mimetypes. Requires mimetype registry from Archetypes >= 1.3. """ mtr = getToolByName(self, 'mimetypes_registry') return mtr.list_mimetypes() security.declareProtected(View, 'getWorkflowChainFor') def getWorkflowChainFor(self, object): """Proxy the request for the chain to the workflow tool, as this method is private there. """ wftool = getToolByName(self, 'portal_workflow') wfs = () try: wfs = wftool.getChainFor(object) except ConflictError: raise except: pass return wfs security.declareProtected(View, 'getIconFor') def getIconFor(self, category, id, default=_marker): """Cache point for actionicons.getActionIcon call. Also we want to allow for a default icon id to be passed in. """ # Short circuit the lookup if (category, id) in _icons.keys(): return _icons[(category, id)] try: actionicons = getToolByName(self, 'portal_actionicons') iconinfo = actionicons.getActionIcon(category, id) icon = _icons.setdefault((category, id), iconinfo) except KeyError: if default is not _marker: icon = default else: raise # We want to return the actual object return icon security.declareProtected(View, 'getReviewStateTitleFor') def getReviewStateTitleFor(self, obj): """Utility method that gets the workflow state title for the object's review_state. Returns None if no review_state found. >>> ptool = self.portal.plone_utils >>> ptool.getReviewStateTitleFor(self.folder) 'Public Draft' """ wf_tool = getToolByName(self, 'portal_workflow') wfs = () review_states = () objstate = None try: objstate = wf_tool.getInfoFor(obj, 'review_state') wfs = wf_tool.getWorkflowsFor(obj) except WorkflowException, e: pass if wfs: for w in wfs: if w.states.has_key(objstate): return w.states[objstate].title or objstate return None security.declareProtected(View, 'getDiscussionThread') def getDiscussionThread(self, discussionContainer): """Given a discussionContainer, return the thread it is in, upwards, including the parent object that is being discussed. """ if safe_hasattr(discussionContainer, 'parentsInThread'): thread = discussionContainer.parentsInThread() if discussionContainer.portal_type == 'Discussion Item': thread.append(discussionContainer) else: if discussionContainer.id=='talkback': thread=[discussionContainer._getDiscussable()] else: thread = [discussionContainer] return thread # Convenience method since skinstool requires loads of acrobatics. # We use this for the reconfig form security.declareProtected(ManagePortal, 'setDefaultSkin') def setDefaultSkin(self, default_skin): """Sets the default skin.""" st = getToolByName(self, 'portal_skins') st.default_skin = default_skin # Set the skin on the page to the specified value # Can be called from a page template, but it must be called before # anything anything on the skin path is resolved (e.g. main_template). # TODO Note: This method will eventually be replaced by the setCurrentSkin # method that is slated for CMF 1.4 security.declarePublic('setCurrentSkin') def setCurrentSkin(self, skin_name): """Sets the current skin.""" portal = getToolByName(self, 'portal_url').getPortalObject() portal.changeSkin(skin_name) security.declareProtected(ManagePortal, 'changeOwnershipOf') def changeOwnershipOf(self, object, userid, recursive=0, REQUEST=None): """Changes the ownership of an object.""" membership = getToolByName(self, 'portal_membership') acl_users = getattr(self, 'acl_users') user = acl_users.getUserById(userid) if user is None: raise KeyError, 'Only retrievable users in this site can be made owners.' object.changeOwnership(user, recursive) def fixOwnerRole(object, user_id): # Get rid of all other owners owners = object.users_with_local_role('Owner') for o in owners: roles = list(object.get_local_roles_for_userid(o)) roles.remove('Owner') if roles: object.manage_setLocalRoles(o, roles) else: object.manage_delLocalRoles([o]) # Fix for 1750 roles = list(object.get_local_roles_for_userid(user_id)) roles.append('Owner') object.manage_setLocalRoles(user_id, roles) fixOwnerRole(object, user.getId()) if base_hasattr(object, 'reindexObject'): object.reindexObject() if recursive: catalog_tool = getToolByName(self, 'portal_catalog') purl = getToolByName(self, 'portal_url') _path = purl.getRelativeContentURL(object) subobjects = [b.getObject() for b in \ catalog_tool(path={'query':_path,'level':1})] for obj in subobjects: fixOwnerRole(obj, user.getId()) if base_hasattr(obj, 'reindexObject'): obj.reindexObject() changeOwnershipOf = postonly(changeOwnershipOf) security.declarePublic('urlparse') def urlparse(self, url): """Returns the pieces of url in a six-part tuple. See Python standard library urlparse.urlparse: http://python.org/doc/lib/module-urlparse.html >>> ptool = self.portal.plone_utils >>> ptool.urlparse('http://dev.plone.org/plone/query?milestone=2.1#foo') ('http', 'dev.plone.org', '/plone/query', '', 'milestone=2.1', 'foo') """ return urlparse.urlparse(url) security.declarePublic('urlunparse') def urlunparse(self, url_tuple): """Puts a url back together again, in the manner that urlparse breaks it. See also Python standard library: urlparse.urlunparse: http://python.org/doc/lib/module-urlparse.html >>> ptool = self.portal.plone_utils >>> ptool.urlunparse(('http', 'plone.org', '/support', '', '', 'users')) 'http://plone.org/support#users' """ return urlparse.urlunparse(url_tuple) # Enable scripts to get the string value of an exception even if the # thrown exception is a string and not a subclass of Exception. def exceptionString(self): # Don't assign the traceback to s # (otherwise will generate a circular reference) s = sys.exc_info()[:2] if s[0] == None: return None if type(s[0]) == type(''): return s[0] return str(s[1]) # Provide a way of dumping an exception to the log even if we # catch it and otherwise ignore it def logException(self): """Dumps most recent exception to the log. """ log_exc() security.declarePublic('createSitemap') def createSitemap(self, context, request=None): """Returns a sitemap navtree structure. """ if request is None: request = self.REQUEST return utils.createSiteMap(context, request) def _addToNavTreeResult(self, result, data): """Adds a piece of content to the result tree. """ return utils.addToNavTreeResult(result, data) security.declareProtected(AccessContentsInformation, 'typesToList') def typesToList(self): return utils.typesToList(self) security.declarePublic('createNavTree') def createNavTree(self, context, sitemap=None, request=None): """Returns a structure that can be used by navigation_tree_slot. """ if request is None: request = self.REQUEST return utils.createNavTree(context, request) security.declarePublic('createTopLevelTabs') def createTopLevelTabs(self, actions=None, context=None, request=None, category='portal_tabs'): """Returns a structure for the top level tabs. """ if context is None: context = self if request is None and hasattr(context, 'REQUEST'): request = context.REQUEST # XXX This method needs to eventually go away utils.log_deprecated("plone_utils.createTopLevelTabs is deprecated and " "will be removed in plone 3.5, please use the " "topLevelTabs method of the INavigationTabs view " "instead.") return utils.createTopLevelTabs(context, request, actions=actions, category=category) security.declarePublic('createBreadCrumbs') def createBreadCrumbs(self, context, request=None): """Returns a structure for the portal breadcumbs. """ if request is None: request = self.REQUEST return utils.createBreadCrumbs(context, request) security.declarePublic('good_id') def good_id(self, id): """Exposes ObjectManager's bad_id test to skin scripts.""" m = bad_id(id) if m is not None: return 0 return 1 security.declarePublic('bad_chars') def bad_chars(self, id): """Returns a list of the Bad characters.""" return BAD_CHARS(id) security.declarePublic('getInheritedLocalRoles') def getInheritedLocalRoles(self, here): """Returns a tuple with the acquired local roles.""" portal = getToolByName(here, 'portal_url').getPortalObject() result = [] cont = 1 if portal != here: parent = here.aq_parent while cont: if not getattr(parent, 'acl_users', False): break userroles = parent.acl_users._getLocalRolesForDisplay(parent) for user, roles, role_type, name in userroles: # Find user in result found = 0 for user2, roles2, type2, name2 in result: if user2 == user: # Check which roles must be added to roles2 for role in roles: if not role in roles2: roles2.append(role) found = 1 break if found == 0: # Add it to result and make sure roles is a list so # we may append and not overwrite the loop variable result.append([user, list(roles), role_type, name]) if parent == portal: cont = 0 elif not self.isLocalRoleAcquired(parent): # Role acquired check here cont = 0 else: parent = parent.aq_parent # Tuplize all inner roles for pos in range(len(result)-1,-1,-1): result[pos][1] = tuple(result[pos][1]) result[pos] = tuple(result[pos]) return tuple(result) # # The three methods used in determining what the default-page of a folder # is. These are: # # - getDefaultPage(folder) # : get id of contentish object that is default-page in the folder # - isDefaultPage(object) # : determine if an object is the default-page in its parent folder # - browserDefault(object) # : lookup rules for old-style content types # security.declarePublic('isDefaultPage') def isDefaultPage(self, obj, request=None): """Finds out if the given obj is the default page in its parent folder. Only considers explicitly contained objects, either set as index_html, with the default_page property, or using IBrowserDefault. """ #XXX: Does this deserve deprecation? # utils.log_deprecated("plone_utils.isDefaultPage is deprecated and " # "will be removed in plone 3.5, please use the " # "isDefaultPage method of the IDefaultPage view " # "on the container.") if request is None: request = self.REQUEST return utils.isDefaultPage(obj, request, context=self) security.declarePublic('getDefaultPage') def getDefaultPage(self, obj, request=None): """Given a folderish item, find out if it has a default-page using the following lookup rules: 1. A content object called 'index_html' wins 2. If the folder implements IBrowserDefault, query this 3. Else, look up the property default_page on the object - Note that in this case, the returned id may *not* be of an object in the folder, since it could be acquired from a parent folder or skin layer 4. Else, look up the property default_page in site_properties for magic ids and test these The id of the first matching item is then used to lookup a translation and if found, its id is returned. If no default page is set, None is returned. If a non-folderish item is passed in, return None always. """ #XXX: Does this deserve deprecation? # utils.log_deprecated("plone_utils.getDefaultPage is deprecated and " # "will be removed in plone 3.5, please use the " # "getDefaultPage method of the IDefaultPage view " # "on the container.") if request is None: request = self.REQUEST return utils.getDefaultPage(obj, request, context=self) security.declarePublic('addPortalMessage') def addPortalMessage(self, message, type='info', request=None): """\ Call this once or more to add messages to be displayed at the top of the web page. Examples: >>> ptool = self.portal.plone_utils >>> ptool.addPortalMessage(u'A random warning message', 'warn') If no type is given it defaults to 'info' >>> ptool.addPortalMessage(u'A random info message') The arguments are: message: a string, with the text message you want to show, or a HTML fragment (see type='structure' below) type: optional, defaults to 'info'. The type determines how the message will be rendered, as it is used to select the CSS class for the message. Predefined types are: 'info' - for informational messages 'warn' - for warning messages 'stop' - for messages about restricted access or errors. Portal messages are by default rendered by the global_statusmessage.pt page template. It is also possible to add messages from page templates, as long as they are processed before the portal_message macro is called by the main template. Example: """ if request is None: request = self.REQUEST IStatusMessage(request).addStatusMessage(message, type=type) security.declarePublic('showPortalMessages') def showPortalMessages(self, request=None): """\ Return portal status messages that will be displayed when the response web page is rendered. Portal status messages are by default rendered by the global_statusmessage.pt page template. They will be removed after they have been shown. See addPortalMessages for examples. """ if request is None: request = self.REQUEST return IStatusMessage(request).showStatusMessages() security.declarePublic('browserDefault') def browserDefault(self, obj): """Sets default so we can return whatever we want instead of index_html. This method is complex, and interacts with mechanisms such as IBrowserDefault (implemented in CMFDynamicViewFTI), LinguaPlone and various mechanisms for setting the default page. The method returns a tuple (obj, [path]) where path is a path to a template or other object to be acquired and displayed on the object. The path is determined as follows: 0. If we're coming from WebDAV, make sure we don't return a contained object "default page" ever 1. If there is an index_html attribute (either a contained object or an explicit attribute) on the object, return that as the "default page". Note that this may be used by things like File and Image to return the contents of the file, for example, not just content-space objects created by the user. 2. If the object implements IBrowserDefault, query this for the default page. 3. If the object has a property default_page set and this gives a list of, or single, object id, and that object is is found in the folder or is the name of a skin template, return that id 4. If the property default_page is set in site_properties and that property contains a list of ids of which one id is found in the folder, return that id 5. If the object implements IBrowserDefault, try to get the selected layout. 6. If the type has a 'folderlisting' action and no default page is set, use this action. This permits folders to have the default 'view' action be 'string:${object_url}/' and hence default to a default page when clicking the 'view' tab, whilst allowing the fallback action to be specified TTW in portal_types (this action is typically hidden) 7. If nothing else is found, fall back on the object's 'view' action. 8. If this is not found, raise an AttributeError If the returned path is an object, it is checked for ITranslatable. An object which supports translation will then be translated before return. """ # WebDAV in Zope is odd it takes the incoming verb eg: PROPFIND # and then requests that object, for example for: /, with verb PROPFIND # means acquire PROPFIND from the folder and call it # its all very odd and WebDAV'y request = getattr(self, 'REQUEST', None) if request and request.has_key('REQUEST_METHOD'): if request['REQUEST_METHOD'] not in ['GET', 'POST']: return obj, [request['REQUEST_METHOD']] # Now back to normal portal = getToolByName(self, 'portal_url').getPortalObject() wftool = getToolByName(self, 'portal_workflow') # Looking up translatable is done several places so we make a # method for it. def returnPage(obj, page): # Only look up for untranslated folderish content, # in translated containers we assume the container has default page # in the correct language. implemented = ITranslatable.isImplementedBy(obj) if not implemented or implemented and not obj.isTranslation(): pageobj = getattr(obj, page, None) if pageobj is not None and ITranslatable.isImplementedBy(pageobj): translation = pageobj.getTranslation() if translation is not None and \ (not wftool.getChainFor(pageobj) or\ wftool.getInfoFor(pageobj, 'review_state') == wftool.getInfoFor(translation, 'review_state')): if ids.has_key(translation.getId()): return obj, [translation.getId()] else: return translation, ['view'] return obj, [page] # The list of ids where we look for default ids = {} # If we are not dealing with a folder, then leave this empty if obj.isPrincipiaFolderish: # For BTreeFolders we just use has_key, otherwise build a dict if base_hasattr(obj, 'has_key'): ids = obj else: for id in obj.objectIds(): ids[id] = 1 # # 1. Get an attribute or contained object index_html # # Note: The base PloneFolder, as well as ATCT's ATCTOrderedFolder # defines a method index_html() which returns a ReplaceableWrapper. # This is needed for WebDAV to work properly, and to avoid implicit # acquisition of index_html's, which are generally on-object only. # For the purposes of determining a default page, we don't want to # use this index_html(), nor the ComputedAttribute which defines it. if not isinstance(getattr(obj, 'index_html', None), ReplaceableWrapper): index_obj = getattr(aq_base(obj), 'index_html', None) if index_obj is not None and not isinstance(index_obj, ComputedAttribute): return returnPage(obj, 'index_html') # # 2. Look for a default_page managed by an IBrowserDefault-implementing # object # # 3. Look for a default_page property on the object # # 4. Try the default sitewide default_page setting # if obj.isPrincipiaFolderish: defaultPage = self.getDefaultPage(obj) if defaultPage is not None: if ids.has_key(defaultPage): return returnPage(obj, defaultPage) # Avoid infinite recursion in the case that the page id == the # object id elif defaultPage != obj.getId() and \ defaultPage != '/'.join(obj.getPhysicalPath()): # For the default_page property, we may get things in the # skin layers or with an explicit path - split this path # to comply with the __browser_default__() spec return obj, defaultPage.split('/') # 5. If there is no default page, try IBrowserDefault.getLayout() browserDefault = IBrowserDefault(obj, None) if browserDefault is not None: layout = browserDefault.getLayout() if layout is None: raise AttributeError( "%s has no assigned layout, perhaps it needs an FTI"%obj) else: return obj, [layout] # # 6. If the object has a 'folderlisting' action, use this # # This allows folders to determine in a flexible manner how they are # displayed when there is no default page, whilst still using # browserDefault() to show contained objects by default on the 'view' # action (this applies to old-style folders only, IBrowserDefault is # managed explicitly above) try: # XXX: This isn't quite right since it assumes the action # starts with ${object_url}. Should we raise an error if # it doesn't? act = obj.getTypeInfo().getActionInfo('folder/folderlisting')['url'].split('/')[-1] return obj, [act] except ValueError: pass # # 7. Fall back on the 'view' action # try: # XXX: This isn't quite right since it assumes the action # starts with ${object_url}. Should we raise an error if # it doesn't? act = obj.getTypeInfo().getActionInfo('object/view')['url'].split('/')[-1] return obj, [act] except ValueError: pass # # 8. If we can't find this either, raise an exception # raise AttributeError, "Failed to get a default page or view_action for %s" % (obj.absolute_url,) security.declarePublic('isTranslatable') def isTranslatable(self, obj): """Checks if a given object implements the ITranslatable interface.""" return ITranslatable.isImplementedBy(obj) security.declarePublic('isStructuralFolder') def isStructuralFolder(self, obj): """Checks if a given object is a "structural folder". That is, a folderish item which does not explicitly implement INonStructuralFolder to declare that it doesn't wish to be treated as a folder by the navtree, the tab generation etc. >>> ptool = self.portal.plone_utils >>> ptool.isStructuralFolder(self.folder) True """ if not obj.isPrincipiaFolderish: return False elif INonStructuralFolder.providedBy(obj): return False else: return True security.declarePublic('acquireLocalRoles') def acquireLocalRoles(self, obj, status = 1, REQUEST=None): """If status is 1, allow acquisition of local roles (regular behaviour). If it's 0, prohibit it (it will allow some kind of local role blacklisting). """ mt = getToolByName(self, 'portal_membership') if not mt.checkPermission(ModifyPortalContent, obj): raise Unauthorized # Set local role status gruf = getToolByName(self, 'portal_url').getPortalObject().acl_users # We perform our own security check gruf._acquireLocalRoles(obj, status) # Reindex the whole stuff. obj.reindexObjectSecurity() acquireLocalRoles = postonly(acquireLocalRoles) security.declarePublic('isLocalRoleAcquired') def isLocalRoleAcquired(self, obj): """Returns local role acquisition blocking status. True if normal, false if blocked. GRUF IS REQUIRED FOR THIS TO WORK. """ gruf = getToolByName(self, 'portal_url').getPortalObject().acl_users return gruf.isLocalRoleAcquired(obj) security.declarePublic('getOwnerName') def getOwnerName(self, obj): """ Returns the userid of the owner of an object. >>> ptool = self.portal.plone_utils >>> from Products.PloneTestCase.PloneTestCase import default_user >>> ptool.getOwnerName(self.folder) == default_user True """ mt = getToolByName(self, 'portal_membership') if not mt.checkPermission(View, obj): raise Unauthorized return obj.getOwner().getUserName() security.declarePublic('normalizeString') def normalizeString(self, text, relaxed=False): """Normalizes a title to an id. normalizeString() converts a whole string to a normalized form that should be safe to use as in a url, as a css id, etc. If relaxed=True, only those characters that are illegal as URLs and leading or trailing whitespace is stripped. >>> ptool = self.portal.plone_utils >>> ptool.normalizeString("Foo bar") 'foo-bar' >>> ptool.normalizeString("Foo bar", relaxed=True) 'Foo bar' >>> ptool.normalizeString("Some!_are allowed, others&?:are not") 'some-_are-allowed-others-are-not' >>> ptool.normalizeString("Some!_are allowed, others&?:are not") 'some-_are-allowed-others-are-not' all punctuation and spacing is removed and replaced with a '-': >>> ptool.normalizeString("a string with spaces") 'a-string-with-spaces' >>> ptool.normalizeString("p.u,n;c(t)u!a@t#i$o%n") 'p-u-n-c-t-u-a-t-i-o-n' strings are lowercased: >>> ptool.normalizeString("UppERcaSE") 'uppercase' punctuation, spaces, etc. are trimmed and multiples are reduced to just one: >>> ptool.normalizeString(" a string ") 'a-string' >>> ptool.normalizeString(">here's another!") 'heres-another' >>> ptool.normalizeString("one with !@#$!@#$ stuff in the middle") 'one-with-stuff-in-the-middle' the exception to all this is that if there is something that looks like a filename with an extension at the end, it will preserve the last period. >>> ptool.normalizeString("this is a file.gif") 'this-is-a-file.gif' >>> ptool.normalizeString("this is. also. a file.html") 'this-is-also-a-file.html' normalizeString() uses normalizeUnicode() to convert stray unicode characters. it will attempt to transliterate many of the accented letters to rough ASCII equivalents: >>> ptool.normalizeString(u"Eksempel \xe6\xf8\xe5 norsk \xc6\xd8\xc5") 'eksempel-eoa-norsk-eoa' for characters that we can't transliterate, we just return the hex codes of the byte(s) in the character. not pretty, but about the best we can do. >>> ptool.normalizeString(u"\u9ad8\u8054\u5408 Chinese") '9ad880545408-chinese' >>> ptool.normalizeString(u"\uc774\ubbf8\uc9f1 Korean") 'c774bbf8c9f1-korean' """ return utils.normalizeString(text, context=self, relaxed=relaxed) security.declarePublic('listMetaTags') def listMetaTags(self, context): """Lists meta tags helper. Creates a mapping of meta tags -> values for the listMetaTags script. """ result = {} site_props = getToolByName(self, 'portal_properties').site_properties use_all = site_props.getProperty('exposeDCMetaTags', None) if not use_all: metadata_names = {'Description': METADATA_DCNAME['Description']} else: metadata_names = METADATA_DCNAME for accessor, key in metadata_names.items(): method = getattr(aq_inner(context).aq_explicit, accessor, None) if not callable(method): continue # Catch AttributeErrors raised by some AT applications try: value = method() except AttributeError: value = None if not value: # No data continue if accessor == 'Publisher' and value == 'No publisher': # No publisher is hardcoded (TODO: still?) continue if isinstance(value, (list, tuple)): # convert a list to a string value = ', '.join(value) # Special cases if accessor == 'Description': result['description'] = value elif accessor == 'Subject': result['keywords'] = value if use_all: result[key] = value if use_all: created = context.CreationDate() try: effective = context.EffectiveDate() if effective == 'None': effective = None if effective: effective = DateTime(effective) except AttributeError: effective = None try: expires = context.ExpirationDate() if expires == 'None': expires = None if expires: expires = DateTime(expires) except AttributeError: expires = None # Filter out DWIMish artifacts on effective / expiration dates if effective is not None and \ effective > FLOOR_DATE and \ effective != created: eff_str = effective.Date() else: eff_str = '' if expires is not None and expires < CEILING_DATE: exp_str = expires.Date() else: exp_str = '' if exp_str or exp_str: result['DC.date.valid_range'] = '%s - %s' % (eff_str, exp_str) return result security.declarePublic('getUserFriendlyTypes') def getUserFriendlyTypes(self, typesList=[]): """Get a list of types which are considered "user friendly" for search and selection purposes. This is the list of types available in the portal, minus those defines in the types_not_searched property in site_properties, if it exists. If typesList is given, this is used as the base list; else all types from portal_types are used. """ ptool = getToolByName(self, 'portal_properties') siteProperties = getattr(ptool, 'site_properties') blacklistedTypes = siteProperties.getProperty('types_not_searched', []) ttool = getToolByName(self, 'portal_types') types = typesList or ttool.listContentTypes() friendlyTypes = [] for t in types: if not t in blacklistedTypes and not t in friendlyTypes: friendlyTypes.append(t) return friendlyTypes security.declarePublic('reindexOnReorder') def reindexOnReorder(self, parent): """ Catalog ordering support """ # For now we will just reindex all objects in the folder. Later we may # optimize to only reindex the objs that got moved. Ordering is more # for humans than machines, therefore the fact that this won't scale # well for btrees isn't a huge issue, since btrees are more for # machines than humans. mtool = getToolByName(self, 'portal_membership') if not mtool.checkPermission(ModifyPortalContent, parent): return cat = getToolByName(self, 'portal_catalog') cataloged_objs = cat(path = {'query':'/'.join(parent.getPhysicalPath()), 'depth': 1}) for brain in cataloged_objs: obj = brain.getObject() # Don't crash when the catalog has contains a stale entry if obj is not None: cat.reindexObject(obj,['getObjPositionInParent'], update_metadata=0) else: # Perhaps we should remove the bad entry as well? log('Object in catalog no longer exists, cannot reindex: %s.'% brain.getPath()) security.declarePublic('isIDAutoGenerated') def isIDAutoGenerated(self, id): """Determine if an id is autogenerated""" return utils.isIDAutoGenerated(self, id) security.declarePublic('getEmptyTitle') def getEmptyTitle(self, translated=True): """ Returns string to be used for objects with no title or id. >>> ptool = self.portal.plone_utils >>> ptool.getEmptyTitle(translated=False) == u'[\xb7\xb7\xb7]' True """ return utils.getEmptyTitle(self, translated) security.declarePublic('pretty_title_or_id') def pretty_title_or_id(self, obj, empty_value=_marker): """Return the best possible title or id of an item, regardless of whether obj is a catalog brain or an object, but returning an empty title marker if the id is not set (i.e. it's auto-generated). """ return utils.pretty_title_or_id(self, obj, empty_value=empty_value) security.declarePublic('getMethodAliases') def getMethodAliases(self, typeInfo): """Given an FTI, return the dict of method aliases defined on that FTI. If there are no method aliases (i.e. this FTI doesn't support it), return None""" getMethodAliases = getattr(typeInfo, 'getMethodAliases', None) if getMethodAliases is not None and utils.safe_callable(getMethodAliases): return getMethodAliases() else: return None InitializeClass(PloneTool)