• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.CMFCore / 6246931310

20 Sep 2023 09:54AM UTC coverage: 86.008% (-0.3%) from 86.266%
6246931310

Pull #131

github

mauritsvanrees
gha: don't need setup-python on 27 as we use the 27 container.
Pull Request #131: Make decodeFolderFilter and encodeFolderFilter non-public.

2466 of 3689 branches covered (0.0%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 1 file covered. (100.0%)

17297 of 19289 relevant lines covered (89.67%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.73
/src/Products/CMFCore/PortalFolder.py
1
##############################################################################
2
#
3
# Copyright (c) 2001 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE.
11
#
12
##############################################################################
13
""" PortalFolder: CMF-enabled Folder objects.
1✔
14
"""
15

16
import marshal
1✔
17
import re
1✔
18

19
import six
1✔
20
from six import get_unbound_function
1✔
21

22
from AccessControl.class_init import InitializeClass
1✔
23
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
24
from AccessControl.SecurityManagement import getSecurityManager
1✔
25
from Acquisition import aq_base
1✔
26
from Acquisition import aq_inner
1✔
27
from Acquisition import aq_parent
1✔
28
from OFS.Folder import Folder
1✔
29
from OFS.OrderSupport import OrderSupport
1✔
30
from zope.component import getUtility
1✔
31
from zope.component import queryUtility
1✔
32
from zope.component.factory import Factory
1✔
33
from zope.interface import implementer
1✔
34

35
from .CMFCatalogAware import OpaqueItemManager
1✔
36
from .DynamicType import DynamicType
1✔
37
from .exceptions import AccessControl_Unauthorized
1✔
38
from .exceptions import BadRequest
1✔
39
from .exceptions import zExceptions_Unauthorized
1✔
40
from .interfaces import IContentTypeRegistry
1✔
41
from .interfaces import IFolderish
1✔
42
from .interfaces import IMutableMinimalDublinCore
1✔
43
from .interfaces import ISiteRoot
1✔
44
from .interfaces import ITypesTool
1✔
45
from .permissions import AddPortalContent
1✔
46
from .permissions import AddPortalFolders
1✔
47
from .permissions import DeleteObjects
1✔
48
from .permissions import ListFolderContents
1✔
49
from .permissions import ManagePortal
1✔
50
from .permissions import ManageProperties
1✔
51
from .permissions import View
1✔
52
from .utils import _checkPermission
1✔
53
from .utils import base64_decode
1✔
54
from .utils import base64_encode
1✔
55

56

57
@implementer(IFolderish, IMutableMinimalDublinCore)
1✔
58
class PortalFolderBase(DynamicType, OpaqueItemManager, Folder):
1✔
59

60
    """Base class for portal folder.
61
    """
62

63
    security = ClassSecurityInfo()
1✔
64

65
    description = ''
1✔
66

67
    manage_options = (Folder.manage_options[:1]
1✔
68
                      + ({'label': 'Components',
69
                          'action': 'manage_components'},)
70
                      + ({'label': 'Components Folder',
71
                          'action': '++etc++site/manage_main'},)
72
                      + Folder.manage_options[1:])
73

74
    def __init__(self, id, title='', description=''):
1✔
75
        self.id = id
1✔
76
        self.title = title
1✔
77
        self.description = description
1✔
78

79
    #
80
    #   'IMutableMinimalDublinCore' interface methods
81
    #
82
    @security.protected(View)
1✔
83
    def Title(self):
1✔
84
        """ Dublin Core Title element - resource name.
85
        """
86
        return self.title
1✔
87

88
    @security.protected(View)
1✔
89
    def Description(self):
1✔
90
        """ Dublin Core Description element - resource summary.
91
        """
92
        return self.description
1✔
93

94
    @security.protected(View)
1✔
95
    def Type(self):
1✔
96
        """ Dublin Core Type element - resource type.
97
        """
98
        ti = self.getTypeInfo()
1✔
99
        return ti is not None and ti.Title() or 'Unknown'
1✔
100

101
    @security.protected(ManageProperties)
1✔
102
    def setTitle(self, title):
1✔
103
        """ Set Dublin Core Title element - resource name.
104
        """
105
        self.title = title
1✔
106

107
    @security.protected(ManageProperties)
1✔
108
    def setDescription(self, description):
1✔
109
        """ Set Dublin Core Description element - resource summary.
110
        """
111
        self.description = description
1✔
112

113
    #
114
    #   other methods
115
    #
116
    @security.protected(ManageProperties)
1✔
117
    def edit(self, title='', description=''):
1✔
118
        """
119
        Edit the folder title (and possibly other attributes later)
120
        """
121
        self.setTitle(title)
×
122
        self.setDescription(description)
×
123
        # BBB: for ICatalogAware subclasses
124
        if getattr(self, 'reindexObject', None) is not None:
×
125
            self.reindexObject()
×
126

127
    @security.public
1✔
128
    def allowedContentTypes(self):
1✔
129
        """
130
            List type info objects for types which can be added in
131
            this folder.
132
        """
133
        ttool = getUtility(ITypesTool)
×
134
        myType = ttool.getTypeInfo(self)
×
135
        result = ttool.listTypeInfo()
×
136

137
        if myType is not None:
×
138
            return [t for t in result if myType.allowType(t.getId()) and
×
139
                    t.isConstructionAllowed(self)]
140

141
        return [t for t in result if t.isConstructionAllowed(self)]
×
142

143
    def _filteredItems(self, ids, filt):
1✔
144
        """
145
            Apply filter, a mapping, to child objects indicated by 'ids',
146
            returning a sequence of (id, obj) tuples.
147
        """
148
        # Restrict allowed content types
149
        if filt is None:
1!
150
            filt = {}
1✔
151
        else:
152
            # We'll modify it, work on a copy.
153
            filt = filt.copy()
×
154
        pt = filt.get('portal_type', [])
1✔
155
        if isinstance(pt, six.string_types):
1!
156
            pt = [pt]
×
157
        ttool = getUtility(ITypesTool)
1✔
158
        allowed_types = ttool.listContentTypes()
1✔
159
        if not pt:
1!
160
            pt = allowed_types
1✔
161
        else:
162
            pt = [t for t in pt if t in allowed_types]
×
163
        if not pt:
1✔
164
            # After filtering, no types remain, so nothing should be
165
            # returned.
166
            return []
1✔
167
        filt['portal_type'] = pt
1✔
168

169
        query = ContentFilter(**filt)
1✔
170
        result = []
1✔
171
        append = result.append
1✔
172
        get = self._getOb
1✔
173
        for id in ids:
1✔
174
            obj = get(id)
1✔
175
            if query(obj):
1✔
176
                append((id, obj))
1✔
177
        return result
1✔
178

179
    #
180
    #   'IFolderish' interface methods
181
    #
182
    @security.public
1✔
183
    def contentItems(self, filter=None):
1✔
184
        # List contentish and folderish sub-objects and their IDs.
185
        # (method is without docstring to disable publishing)
186
        #
187
        ids = self.objectIds()
1✔
188
        return self._filteredItems(ids, filter)
1✔
189

190
    @security.public
1✔
191
    def contentIds(self, filter=None):
1✔
192
        # List IDs of contentish and folderish sub-objects.
193
        # (method is without docstring to disable publishing)
194
        #
195
        return [item[0] for item in self.contentItems(filter)]
1✔
196

197
    @security.public
1✔
198
    def contentValues(self, filter=None):
1✔
199
        # List contentish and folderish sub-objects.
200
        # (method is without docstring to disable publishing)
201
        #
202
        return [item[1] for item in self.contentItems(filter)]
1✔
203

204
    @security.protected(ListFolderContents)
1✔
205
    def listFolderContents(self, contentFilter=None):
1✔
206
        """ List viewable contentish and folderish sub-objects.
207
        """
208
        fc_list = []
1✔
209
        for id, obj in self.contentItems(contentFilter):
1✔
210
            # validate() can either raise Unauthorized or return 0 to
211
            # mean unauthorized.
212
            try:
1✔
213
                if getSecurityManager().validate(self, self, id, obj):
1✔
214
                    fc_list.append(obj)
1✔
215
            except zExceptions_Unauthorized:  # Catch *all* Unauths!
×
216
                pass
×
217
        return fc_list
1✔
218

219
    #
220
    #   webdav Resource method
221
    #
222

223
    # protected by 'WebDAV access'
224
    def listDAVObjects(self):
1✔
225
        # List sub-objects for PROPFIND requests.
226
        # (method is without docstring to disable publishing)
227
        #
228
        if _checkPermission(ManagePortal, self):
1!
229
            return self.objectValues()
1✔
230
        else:
231
            return self.listFolderContents()
×
232

233
    #
234
    #   other methods
235
    #
236
    def encodeFolderFilter(self, REQUEST):
1✔
237
        # Parse cookie string for using variables in dtml.
238
        filter = {}
×
239
        for key, value in REQUEST.items():
×
240
            if key[:10] == 'filter_by_':
×
241
                filter[key[10:]] = value
×
242
        encoded = base64_encode(marshal.dumps(filter))
×
243
        encoded = ''.join(encoded.split('\n'))
×
244
        return encoded
×
245

246
    def decodeFolderFilter(self, encoded):
1✔
247
        # Parse cookie string for using variables in dtml.
248
        filter = {}
×
249
        if encoded:
×
250
            filter.update(marshal.loads(base64_decode(encoded)))
×
251
        return filter
×
252

253
    def content_type(self):
1✔
254
        """
255
            WebDAV needs this to do the Right Thing (TM).
256
        """
257
        return None
×
258

259
    def PUT_factory(self, name, typ, body):
1✔
260
        """ Factory for PUT requests to objects which do not yet exist.
261

262
        Used by NullResource.PUT.
263

264
        Returns -- Bare and empty object of the appropriate type (or None, if
265
        we don't know what to do)
266
        """
267
        ctr = queryUtility(IContentTypeRegistry)
×
268
        if ctr is None:
×
269
            return None
×
270

271
        typeObjectName = ctr.findTypeName(name, typ, body)
×
272
        if typeObjectName is None:
×
273
            return None
×
274

275
        self.invokeFactory(typeObjectName, name)
×
276

277
        # invokeFactory does too much, so the object has to be removed again
278
        obj = aq_base(self._getOb(name))
×
279
        self._delObject(name)
×
280
        return obj
×
281

282
    @security.protected(AddPortalContent)
1✔
283
    def invokeFactory(self, type_name, id, RESPONSE=None, *args, **kw):
1✔
284
        """ Invokes the portal_types tool.
285
        """
286
        ttool = getUtility(ITypesTool)
1✔
287
        myType = ttool.getTypeInfo(self)
1✔
288

289
        if myType is not None:
1!
290
            if not myType.allowType(type_name):
1✔
291
                raise ValueError('Disallowed subobject type: %s' % type_name)
1✔
292

293
        return ttool.constructContent(type_name, self, id, RESPONSE,
1✔
294
                                      *args, **kw)
295

296
    @security.protected(AddPortalContent)
1✔
297
    def checkIdAvailable(self, id):
1✔
298
        try:
1✔
299
            self._checkId(id)
1✔
300
        except BadRequest:
1✔
301
            return False
1✔
302
        else:
303
            return True
1✔
304

305
    def MKCOL_handler(self, id, REQUEST=None, RESPONSE=None):
1✔
306
        """
307
            Handle WebDAV MKCOL.
308
        """
309
        self.manage_addFolder(id=id, title='')
×
310

311
    def _checkId(self, id, allow_dup=0):
1✔
312
        PortalFolderBase.inheritedAttribute('_checkId')(self, id, allow_dup)
1✔
313

314
        if allow_dup:
1!
315
            return
×
316

317
        # FIXME: needed to allow index_html for join code
318
        if id == 'index_html':
1!
319
            return
×
320

321
        # Another exception: Must allow "syndication_information" to enable
322
        # Syndication...
323
        if id == 'syndication_information':
1!
324
            return
×
325

326
        # IDs starting with '@@' are reserved for views.
327
        if id[:2] == '@@':
1!
328
            raise BadRequest('The id "%s" is invalid because it begins with '
×
329
                             '"@@".' % id)
330

331
        # This code prevents people other than the portal manager from
332
        # overriding skinned names and tools.
333
        if not getSecurityManager().checkPermission(ManagePortal, self):
1✔
334
            ob = aq_inner(self)
1✔
335
            while ob is not None:
1✔
336
                if ISiteRoot.providedBy(ob):
1✔
337
                    break
1✔
338
                ob = aq_parent(ob)
1✔
339

340
            if ob is not None:
1✔
341
                # If the portal root has a non-contentish object by this name,
342
                # don't allow an override.
343
                if hasattr(ob, id) and \
1✔
344
                   id not in ob.contentIds() and \
345
                   not id.startswith('.'):
346
                    raise BadRequest('The id "%s" is reserved.' % id)
1✔
347
            # Don't allow ids used by Method Aliases.
348
            ti = self.getTypeInfo()
1✔
349
            if ti and ti.queryMethodID(id, context=self):
1✔
350
                raise BadRequest('The id "%s" is reserved.' % id)
1✔
351
        # Otherwise we're ok.
352

353
    def _verifyObjectPaste(self, object, validate_src=1):
1✔
354
        # This assists the version in OFS.CopySupport.
355
        # It enables the clipboard to function correctly
356
        # with objects created by a multi-factory.
357
        mt = getattr(object, '__factory_meta_type__', None)
1✔
358
        meta_types = getattr(self, 'all_meta_types', None)
1✔
359

360
        if mt is not None and meta_types is not None:
1!
361
            mt_permission = None
×
362

363
            if callable(meta_types):
×
364
                meta_types = meta_types()
×
365

366
            for d in meta_types:
×
367
                if d['name'] == mt:
×
368
                    mt_permission = d.get('permission')
×
369
                    break
×
370

371
            if mt_permission is not None:
×
372
                sm = getSecurityManager()
×
373

374
                if sm.checkPermission(mt_permission, self):
×
375
                    if validate_src:
×
376
                        # Ensure the user is allowed to access the object on
377
                        # the clipboard.
378
                        parent = aq_parent(aq_inner(object))
×
379

380
                        if not sm.validate(None, parent, None, object):
×
381
                            raise AccessControl_Unauthorized(object.getId())
×
382

383
                        if validate_src == 2:  # moving
×
384
                            if not sm.checkPermission(DeleteObjects, parent):
×
385
                                raise AccessControl_Unauthorized('Delete not '
×
386
                                                                 'allowed.')
387
                else:
388
                    raise AccessControl_Unauthorized(
×
389
                            'You do not possess the '
390
                            '%r permission in the context of the container '
391
                            'into which you are pasting, thus you are not '
392
                            'able to perform this operation.' % mt_permission)
393
            else:
394
                raise AccessControl_Unauthorized(
×
395
                        'The object %r does not '
396
                        'support this operation.' % object.getId())
397
        else:
398
            # Call OFS' _verifyObjectPaste if necessary
399
            PortalFolderBase.inheritedAttribute(
1✔
400
                '_verifyObjectPaste')(self, object, validate_src)
401

402
        # Finally, check allowed content types
403
        if hasattr(aq_base(object), 'getPortalTypeName'):
1✔
404

405
            type_name = object.getPortalTypeName()
1✔
406

407
            if type_name is not None:
1!
408

409
                ttool = getUtility(ITypesTool)
1✔
410
                myType = ttool.getTypeInfo(self)
1✔
411

412
                if myType is not None and not myType.allowType(type_name):
1✔
413
                    raise ValueError('Disallowed subobject type: %s' %
1✔
414
                                     type_name)
415

416
                # Check for workflow guards
417
                objType = ttool.getTypeInfo(type_name)
1✔
418
                if objType is not None and \
1✔
419
                   not objType._checkWorkflowAllowed(self):
420
                    raise ValueError('Pasting not allowed in this workflow')
1✔
421

422
    security.setPermissionDefault(AddPortalContent, ('Owner', 'Manager'))
1✔
423

424
    @security.protected(AddPortalFolders)
1✔
425
    def manage_addFolder(self, id, title='', REQUEST=None):
1✔
426
        """ Add a new folder-like object with id *id*.
427

428
        IF present, use the parent object's 'mkdir' alias; otherwise, just add
429
        a PortalFolder.
430
        """
431
        ti = self.getTypeInfo()
1✔
432
        method_id = ti and ti.queryMethodID('mkdir', context=self)
1✔
433
        if method_id:
1✔
434
            # call it
435
            getattr(self, method_id)(id=id)
1✔
436
        else:
437
            self.invokeFactory(type_name='Folder', id=id)
1✔
438

439
        ob = self._getOb(id)
1✔
440
        ob.setTitle(title)
1✔
441
        try:
1✔
442
            ob.reindexObject()
1✔
443
        except AttributeError:
1✔
444
            pass
1✔
445

446
        if REQUEST is not None:
1!
447
            return self.manage_main(self, REQUEST, update_menu=1)
×
448

449

450
InitializeClass(PortalFolderBase)
1✔
451

452

453
class PortalFolder(OrderSupport, PortalFolderBase):
1✔
454

455
    """Implements portal content management, but not UI details.
456
    """
457

458
    portal_type = 'Folder'
1✔
459

460
    security = ClassSecurityInfo()
1✔
461

462
    manage_options = (OrderSupport.manage_options +
1✔
463
                      PortalFolderBase.manage_options[1:])
464

465
    @security.protected(AddPortalFolders)
1✔
466
    def manage_addPortalFolder(self, id, title='', REQUEST=None):
1✔
467
        """Add a new PortalFolder object with id *id*.
468
        """
469
        ob = PortalFolder(id, title)
×
470
        self._setObject(id, ob, suppress_events=True)
×
471
        if REQUEST is not None:
×
472
            return self.folder_contents(  # XXX: ick!
×
473
                self, REQUEST, portal_status_message='Folder added')
474

475

476
InitializeClass(PortalFolder)
1✔
477

478
PortalFolderFactory = Factory(PortalFolder)
1✔
479

480
manage_addPortalFolder = get_unbound_function(
1✔
481
                            PortalFolder.manage_addPortalFolder)
482

483

484
class ContentFilter:
1✔
485

486
    """Represent a predicate against a content object's metadata.
487
    """
488

489
    MARKER = []
1✔
490
    filterSubject = []
1✔
491

492
    def __init__(self, Title=MARKER, Creator=MARKER, Subject=MARKER,
1✔
493
                 Description=MARKER, created=MARKER, created_usage='range:min',
494
                 modified=MARKER, modified_usage='range:min', Type=MARKER,
495
                 portal_type=MARKER, **Ignored):
496

497
        self.predicates = []
1✔
498
        self.description = []
1✔
499

500
        if Title is not self.MARKER:
1✔
501
            self.predicates.append(lambda x, pat=re.compile(Title):
1✔
502
                                   pat.search(x.Title()))
503
            self.description.append('Title: %s' % Title)
1✔
504

505
        if Creator and Creator is not self.MARKER:
1✔
506
            self.predicates.append(lambda x, creator=Creator:
1✔
507
                                   creator in x.listCreators())
508
            self.description.append('Creator: %s' % Creator)
1✔
509

510
        if Subject and Subject is not self.MARKER:
1✔
511
            self.filterSubject = Subject
1✔
512
            self.predicates.append(self.hasSubject)
1✔
513
            self.description.append('Subject: %s' % ', '.join(Subject))
1✔
514

515
        if Description is not self.MARKER:
1✔
516
            self.predicates.append(lambda x, pat=re.compile(Description):
1✔
517
                                   pat.search(x.Description()))
518
            self.description.append('Description: %s' % Description)
1✔
519

520
        if created is not self.MARKER:
1✔
521
            if created_usage == 'range:min':
1✔
522
                self.predicates.append(lambda x, cd=created:
1✔
523
                                       cd <= x.created())
524
                self.description.append('Created since: %s' % created)
1✔
525
            if created_usage == 'range:max':
1✔
526
                self.predicates.append(lambda x, cd=created:
1✔
527
                                       cd >= x.created())
528
                self.description.append('Created before: %s' % created)
1✔
529

530
        if modified is not self.MARKER:
1✔
531
            if modified_usage == 'range:min':
1✔
532
                self.predicates.append(lambda x, md=modified:
1✔
533
                                       md <= x.modified())
534
                self.description.append('Modified since: %s' % modified)
1✔
535
            if modified_usage == 'range:max':
1✔
536
                self.predicates.append(lambda x, md=modified:
1✔
537
                                       md >= x.modified())
538
                self.description.append('Modified before: %s' % modified)
1✔
539

540
        if Type:
1✔
541
            if isinstance(Type, six.string_types):
1✔
542
                Type = [Type]
1✔
543
            self.predicates.append(lambda x, Type=Type: x.Type() in Type)
1✔
544
            self.description.append('Type: %s' % ', '.join(Type))
1✔
545

546
        if portal_type and portal_type is not self.MARKER:
1✔
547
            if isinstance(portal_type, six.string_types):
1✔
548
                portal_type = [portal_type]
1✔
549
            self.predicates.append(lambda x, pt=portal_type:
1✔
550
                                   hasattr(aq_base(x), 'getPortalTypeName')
551
                                   and x.getPortalTypeName() in pt)
552
            self.description.append('Portal Type: %s' % ', '.join(portal_type))
1✔
553

554
    def hasSubject(self, obj):
1✔
555
        """
556
        Converts Subject string into a List for content filter view.
557
        """
558
        for sub in obj.Subject():
1✔
559
            if sub in self.filterSubject:
1✔
560
                return 1
1✔
561
        return 0
1✔
562

563
    def __call__(self, content):
1✔
564

565
        for predicate in self.predicates:
1✔
566

567
            try:
1✔
568
                if not predicate(content):
1✔
569
                    return 0
1✔
570
            except (AttributeError, KeyError, IndexError, ValueError):
1✔
571
                # predicates are *not* allowed to throw exceptions
572
                return 0
1✔
573

574
        return 1
1✔
575

576
    def __str__(self):
1✔
577
        """
578
            Return a stringified description of the filter.
579
        """
580
        return '; '.join(self.description)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc