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

zopefoundation / Products.CMFCore / 18014326785

25 Sep 2025 04:35PM UTC coverage: 86.063%. Remained the same
18014326785

push

github

web-flow
Configuring for zope-product (#149)

1900 of 3044 branches covered (62.42%)

Branch coverage included in aggregate %.

17249 of 19206 relevant lines covered (89.81%)

0.9 hits per line

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

66.82
/src/Products/CMFCore/utils.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
""" Utility functions.
14
"""
15

16
import base64
1✔
17
import re
1✔
18
import sys
1✔
19
from _thread import allocate_lock
1✔
20
from copy import deepcopy
1✔
21
from importlib.metadata import PackageNotFoundError
1✔
22
from importlib.metadata import distribution
1✔
23
from os import path as os_path
1✔
24
from os.path import abspath
1✔
25
from warnings import warn
1✔
26

27
from AccessControl.class_init import InitializeClass
1✔
28
from AccessControl.Permission import Permission
1✔
29
from AccessControl.PermissionRole import rolesForPermissionOn
1✔
30
from AccessControl.rolemanager import gather_permissions
1✔
31
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
32
from AccessControl.SecurityInfo import ModuleSecurityInfo
1✔
33
from AccessControl.SecurityManagement import getSecurityManager
1✔
34
from Acquisition import Implicit
1✔
35
from Acquisition import aq_get
1✔
36
from Acquisition import aq_parent
1✔
37
from Acquisition.interfaces import IAcquirer
1✔
38
from App.Common import package_home
1✔
39
from App.ImageFile import ImageFile
1✔
40
from App.special_dtml import HTMLFile
1✔
41
from DateTime.DateTime import DateTime
1✔
42
from DateTime.interfaces import DateTimeError
1✔
43
from ExtensionClass import Base
1✔
44
from OFS.misc_ import Misc_ as MiscImage
1✔
45
from OFS.misc_ import misc_ as misc_images
1✔
46
from OFS.ObjectManager import UNIQUE
1✔
47
from OFS.PropertyManager import PropertyManager
1✔
48
from OFS.SimpleItem import SimpleItem
1✔
49
from zope.component import getUtility
1✔
50
from zope.component import queryUtility
1✔
51
from zope.datetime import rfc1123_date
1✔
52
from zope.dottedname.resolve import resolve as resolve_dotted_name
1✔
53
from zope.i18nmessageid import MessageFactory
1✔
54
from zope.interface.interfaces import ComponentLookupError
1✔
55

56
import Products
1✔
57

58
from .exceptions import AccessControl_Unauthorized
1✔
59
from .exceptions import NotFound
1✔
60
from .interfaces import ICachingPolicyManager
1✔
61

62

63
HAS_ZSERVER = True
1✔
64
try:
1✔
65
    dist = distribution('ZServer')
1✔
66
except PackageNotFoundError:
1✔
67
    HAS_ZSERVER = False
1✔
68

69
SUBTEMPLATE = '__SUBTEMPLATE__'
1✔
70
ProductsPath = [abspath(ppath) for ppath in Products.__path__]
1✔
71
security = ModuleSecurityInfo('Products.CMFCore.utils')
1✔
72

73
_globals = globals()
1✔
74
_dtmldir = os_path.join(package_home(globals()), 'dtml')
1✔
75
_wwwdir = os_path.join(package_home(globals()), 'www')
1✔
76

77
#
78
#   Simple utility functions, callable from restricted code.
79
#
80
_marker = []  # Create a new marker object.
1✔
81

82
_tool_interface_registry = {}
1✔
83

84

85
@security.private
1✔
86
def registerToolInterface(tool_id, tool_interface):
1✔
87
    """ Register a tool ID for an interface
88

89
    This method can go away when getToolByName is going away
90
    """
91
    _tool_interface_registry[tool_id] = tool_interface
1✔
92

93

94
@security.private
1✔
95
def getToolInterface(tool_id):
1✔
96
    """ Get the interface registered for a tool ID
97
    """
98
    return _tool_interface_registry.get(tool_id, None)
×
99

100

101
@security.public
1✔
102
def getToolByName(obj, name, default=_marker):
1✔
103
    """ Get the tool, 'toolname', by acquiring it.
104

105
    o Application code should use this method, rather than simply
106
      acquiring the tool by name, to ease forward migration (e.g.,
107
      to Zope3).
108
    """
109
    tool_interface = _tool_interface_registry.get(name)
1✔
110

111
    if tool_interface is not None:
1✔
112
        try:
1✔
113
            utility = getUtility(tool_interface)
1✔
114
            # Site managers, except for five.localsitemanager, return unwrapped
115
            # utilities. If the result is something which is
116
            # acquisition-unaware but unwrapped we wrap it on the context.
117
            if IAcquirer.providedBy(obj) and \
1!
118
                    aq_parent(utility) is None and \
119
                    IAcquirer.providedBy(utility):
120
                utility = utility.__of__(obj)
1✔
121
            return utility
1✔
122
        except ComponentLookupError:
1✔
123
            # behave in backwards-compatible way
124
            # fall through to old implementation
125
            pass
1✔
126

127
    try:
1✔
128
        tool = aq_get(obj, name, default, 1)
1✔
129
    except AttributeError:
×
130
        if default is _marker:
×
131
            raise
×
132
        return default
×
133
    else:
134
        if tool is _marker:
1!
135
            raise AttributeError(name)
×
136
        return tool
1✔
137

138

139
@security.public
1✔
140
def getUtilityByInterfaceName(dotted_name, default=_marker):
1✔
141
    """ Get a tool by its fully-qualified dotted interface path
142

143
    This method replaces getToolByName for use in untrusted code.
144
    Trusted code should use zope.component.getUtility instead.
145
    """
146
    try:
×
147
        iface = resolve_dotted_name(dotted_name)
×
148
    except ImportError:
149
        if default is _marker:
150
            raise ComponentLookupError(dotted_name)
151
        return default
152

153
    try:
×
154
        return getUtility(iface)
×
155
    except ComponentLookupError:
×
156
        if default is _marker:
×
157
            raise
×
158
        return default
×
159

160

161
@security.public
1✔
162
def cookString(text):
1✔
163
    """ Make a Zope-friendly ID from 'text'.
164

165
    o Remove any spaces
166

167
    o Lowercase the ID.
168
    """
169
    rgx = re.compile(r'(^_|[^a-zA-Z0-9-_~\,\.])')
×
170
    cooked = re.sub(rgx, '', text).lower()
×
171
    return cooked
×
172

173

174
@security.public
1✔
175
def tuplize(valueName, value):
1✔
176
    """ Make a tuple from 'value'.
177

178
    o Use 'valueName' to generate appropriate error messages.
179
    """
180
    if isinstance(value, tuple):
×
181
        return value
×
182
    if isinstance(value, list):
×
183
        return tuple(value)
×
184
    if isinstance(value, str):
×
185
        return tuple(value.split())
×
186
    raise ValueError('%s of unsupported type' % valueName)
×
187

188

189
#
190
#   Security utilities, callable only from unrestricted code.
191
#
192
# deprecated alias
193
@security.private
1✔
194
def _getAuthenticatedUser(self):
1✔
195
    return getSecurityManager().getUser()
×
196

197

198
@security.private
1✔
199
def _checkPermission(permission, obj):
1✔
200
    if not isinstance(permission, str):
1!
201
        permission = permission.decode()
×
202
    return getSecurityManager().checkPermission(permission, obj)
1✔
203

204

205
# If Zope ever provides a call to getRolesInContext() through
206
# the SecurityManager API, the method below needs to be updated.
207
@security.private
1✔
208
def _limitGrantedRoles(roles, context, special_roles=()):
1✔
209
    # Only allow a user to grant roles already possessed by that user,
210
    # with the exception that all special_roles can also be granted.
211
    user = getSecurityManager().getUser()
×
212
    if user is None:
×
213
        user_roles = ()
×
214
    else:
215
        user_roles = user.getRolesInContext(context)
×
216
    if 'Manager' in user_roles:
×
217
        # Assume all other roles are allowed.
218
        return
×
219
    for role in roles:
×
220
        if role not in special_roles and role not in user_roles:
×
221
            raise AccessControl_Unauthorized('Too many roles specified.')
×
222

223

224
@security.private
1✔
225
def _mergedLocalRoles(object):
1✔
226
    """Returns a merging of object and its ancestors'
227
    __ac_local_roles__."""
228
    # Modified from AccessControl.User.getRolesInContext().
229
    merged = {}
1✔
230
    object = getattr(object, 'aq_inner', object)
1✔
231
    while 1:
1✔
232
        if hasattr(object, '__ac_local_roles__'):
1✔
233
            dict = object.__ac_local_roles__ or {}
1✔
234
            if callable(dict):
1!
235
                dict = dict()
×
236
            for k, v in dict.items():
1✔
237
                if k in merged:
1!
238
                    merged[k] = merged[k] + v
×
239
                else:
240
                    merged[k] = v
1✔
241
        if hasattr(object, 'aq_parent'):
1!
242
            object = object.aq_parent
×
243
            object = getattr(object, 'aq_inner', object)
×
244
            continue
×
245
        if hasattr(object, '__self__'):
1✔
246
            object = object.__self__
1✔
247
            object = getattr(object, 'aq_inner', object)
1✔
248
            continue
1✔
249
        break
1✔
250

251
    return deepcopy(merged)
1✔
252

253

254
@security.private
1✔
255
def _ac_inherited_permissions(ob, all=0):
1✔
256
    # Get all permissions not defined in ourself that are inherited
257
    # This will be a sequence of tuples with a name as the first item and
258
    # an empty tuple as the second.
259
    d = {}
×
260
    perms = getattr(ob, '__ac_permissions__', ())
×
261
    for p in perms:
×
262
        d[p[0]] = None
×
263
    r = gather_permissions(ob.__class__, [], d)
×
264
    if all:
×
265
        if hasattr(ob, '_subobject_permissions'):
×
266
            for p in ob._subobject_permissions():
×
267
                pname = p[0]
×
268
                if pname not in d:
×
269
                    d[pname] = 1
×
270
                    r.append(p)
×
271
        r = list(perms) + r
×
272
    return r
×
273

274

275
@security.private
1✔
276
def _modifyPermissionMappings(ob, map):
1✔
277
    """
278
    Modifies multiple role to permission mappings.
279
    """
280
    # This mimics what AccessControl/Role.py does.
281
    # Needless to say, it's crude. :-(
282
    something_changed = 0
×
283
    perm_info = _ac_inherited_permissions(ob, 1)
×
284
    for name, settings in map.items():
×
285
        cur_roles = rolesForPermissionOn(name, ob)
×
286
        if isinstance(cur_roles, str):
×
287
            cur_roles = [cur_roles]
×
288
        else:
289
            cur_roles = list(cur_roles)
×
290
        changed = 0
×
291
        for (role, allow) in settings.items():
×
292
            if not allow:
×
293
                if role in cur_roles:
×
294
                    changed = 1
×
295
                    cur_roles.remove(role)
×
296
            else:
297
                if role not in cur_roles:
×
298
                    changed = 1
×
299
                    cur_roles.append(role)
×
300
        if changed:
×
301
            data = ()  # The list of methods using this permission.
×
302
            for perm in perm_info:
×
303
                n, d = perm[:2]
×
304
                if n == name:
×
305
                    data = d
×
306
                    break
×
307
            p = Permission(name, data, ob)
×
308
            p.setRoles(tuple(cur_roles))
×
309
            something_changed = 1
×
310
    return something_changed
×
311

312

313
class FakeExecutableObject:
1✔
314

315
    """Fake ExecutableObject used to set proxy roles in trusted code.
316
    """
317

318
    def __init__(self, proxy_roles):
1✔
319
        self._proxy_roles = tuple(proxy_roles)
1✔
320

321
    def getOwner(self):
1✔
322
        return None
1✔
323

324
    getWrappedOwner = getOwner
1✔
325

326

327
# Parse a string of etags from an If-None-Match header
328
# Code follows ZPublisher.HTTPRequest.parse_cookie
329
parse_etags_lock = allocate_lock()
1✔
330

331

332
def parse_etags(text,
1✔
333
                result=None,
334
                # quoted etags (assumed separated by whitespace + a comma)
335
                etagre_quote=re.compile(r'(\s*\"([^\"]*)\"\s*,{0,1})'),
336
                # non-quoted etags (assumed separated by whitespace + a comma)
337
                etagre_noquote=re.compile(r'(\s*([^,]*)\s*,{0,1})'),
338
                acquire=parse_etags_lock.acquire,
339
                release=parse_etags_lock.release):
340

341
    if result is None:
1✔
342
        result = []
1✔
343
    if not len(text):
1✔
344
        return result
1✔
345

346
    acquire()
1✔
347
    try:
1✔
348
        m = etagre_quote.match(text)
1✔
349
        if m:
1✔
350
            # Match quoted etag (spec-observing client)
351
            tl = len(m.group(1))
1✔
352
            value = m.group(2)
1✔
353
        else:
354
            # Match non-quoted etag (lazy client)
355
            m = etagre_noquote.match(text)
1✔
356
            if m:
1!
357
                tl = len(m.group(1))
1✔
358
                value = m.group(2)
1✔
359
            else:
360
                return result
×
361
    finally:
362
        release()
1✔
363

364
    if value:
1!
365
        result.append(value)
1✔
366
    return parse_etags(*(text[tl:], result))
1✔
367

368

369
def _checkConditionalGET(obj, extra_context):
1✔
370
    """A conditional GET is done using one or both of the request
371
       headers:
372

373
       If-Modified-Since: Date
374
       If-None-Match: list ETags (comma delimited, sometimes quoted)
375

376
       If both conditions are present, both must be satisfied.
377

378
       This method checks the caching policy manager to see if
379
       a content object's Last-modified date and ETag satisfy
380
       the conditional GET headers.
381

382
       Returns the tuple (last_modified, etag) if the conditional
383
       GET requirements are met and None if not.
384

385
       It is possible for one of the tuple elements to be None.
386
       For example, if there is no If-None-Match header and
387
       the caching policy does not specify an ETag, we will
388
       just return (last_modified, None).
389
       """
390

391
    REQUEST = getattr(obj, 'REQUEST', None)
1✔
392
    if REQUEST is None:
1!
393
        return False
×
394

395
    # check whether we need to suppress subtemplates
396
    call_count = getattr(REQUEST, SUBTEMPLATE, 0)
1✔
397
    setattr(REQUEST, SUBTEMPLATE, call_count + 1)
1✔
398
    if call_count != 0:
1✔
399
        return False
1✔
400

401
    if_modified_since = REQUEST.getHeader('If-Modified-Since', None)
1✔
402
    if_none_match = REQUEST.getHeader('If-None-Match', None)
1✔
403

404
    if if_modified_since is None and if_none_match is None:
1✔
405
        # not a conditional GET
406
        return False
1✔
407

408
    manager = queryUtility(ICachingPolicyManager)
1✔
409
    if manager is None:
1✔
410
        return False
1✔
411

412
    ret = manager.getModTimeAndETag(aq_parent(obj), obj.getId(), extra_context)
1✔
413
    if ret is None:
1✔
414
        # no appropriate policy or 304s not enabled
415
        return False
1✔
416

417
    (content_mod_time, content_etag, set_last_modified_header) = ret
1✔
418
    if content_mod_time:
1✔
419
        mod_time_secs = int(content_mod_time.timeTime())
1✔
420
    else:
421
        mod_time_secs = None
1✔
422

423
    if if_modified_since:
1✔
424
        # from CMFCore/FSFile.py:
425
        if_modified_since = if_modified_since.split(';')[0]
1✔
426
        # Some proxies seem to send invalid date strings for this
427
        # header. If the date string is not valid, we ignore it
428
        # rather than raise an error to be generally consistent
429
        # with common servers such as Apache (which can usually
430
        # understand the screwy date string as a lucky side effect
431
        # of the way they parse it).
432
        try:
1✔
433
            if_modified_since = int(DateTime(if_modified_since).timeTime())
1✔
434
        except Exception:
×
435
            if_modified_since = None
×
436

437
    client_etags = None
1✔
438
    if if_none_match:
1✔
439
        client_etags = parse_etags(if_none_match)
1✔
440

441
    if not if_modified_since and not client_etags:
1!
442
        # not a conditional GET, or headers are messed up
443
        return False
×
444

445
    if if_modified_since:
1✔
446
        if not content_mod_time or \
1✔
447
           mod_time_secs < 0 or \
448
           mod_time_secs > if_modified_since:
449
            return False
1✔
450

451
    if client_etags:
1✔
452
        if not content_etag or \
1✔
453
           (content_etag not in client_etags and '*' not in client_etags):
454
            return False
1✔
455
    else:
456
        # If we generate an ETag, don't validate the conditional GET unless
457
        # the client supplies an ETag
458
        # This may be more conservative than the spec requires, but we are
459
        # already _way_ more conservative.
460
        if content_etag:
1✔
461
            return False
1✔
462

463
    response = REQUEST.RESPONSE
1✔
464
    if content_mod_time and set_last_modified_header:
1!
465
        response.setHeader('Last-modified', str(content_mod_time))
1✔
466
    if content_etag:
1✔
467
        response.setHeader('ETag', content_etag, literal=1)
1✔
468
    response.setStatus(304)
1✔
469
    delattr(REQUEST, SUBTEMPLATE)
1✔
470

471
    return True
1✔
472

473

474
@security.private
1✔
475
def _setCacheHeaders(obj, extra_context):
1✔
476
    """Set cache headers according to cache policy manager for the obj."""
477
    REQUEST = getattr(obj, 'REQUEST', None)
1✔
478

479
    if REQUEST is not None:
1!
480
        call_count = getattr(REQUEST, SUBTEMPLATE, 1) - 1
1✔
481
        setattr(REQUEST, SUBTEMPLATE, call_count)
1✔
482
        if call_count != 0:
1✔
483
            return
1✔
484

485
        # cleanup
486
        delattr(REQUEST, SUBTEMPLATE)
1✔
487

488
        content = aq_parent(obj)
1✔
489
        manager = queryUtility(ICachingPolicyManager)
1✔
490
        if manager is None:
1✔
491
            return
1✔
492

493
        view_name = obj.getId()
1✔
494
        headers = manager.getHTTPCachingHeaders(
1✔
495
            content, view_name, extra_context)
496
        RESPONSE = REQUEST['RESPONSE']
1✔
497
        for key, value in headers:
1✔
498
            if key == 'ETag':
1✔
499
                RESPONSE.setHeader(key, value, literal=1)
1✔
500
            else:
501
                RESPONSE.setHeader(key, value)
1✔
502
        if headers:
1✔
503
            RESPONSE.setHeader('X-Cache-Headers-Set-By',
1✔
504
                               'CachingPolicyManager: %s' %
505
                               '/'.join(manager.getPhysicalPath()))
506

507

508
class _ViewEmulator(Implicit):
1✔
509
    """Auxiliary class used to adapt FSFile and FSImage
510
    for caching_policy_manager
511
    """
512

513
    def __init__(self, view_name=''):
1✔
514
        self._view_name = view_name
1✔
515

516
    def getId(self):
1✔
517
        return self._view_name
1✔
518

519

520
#
521
#   Base classes for tools
522
#
523
class ImmutableId(Base):
1✔
524

525
    """ Base class for objects which cannot be renamed.
526
    """
527

528
    def _setId(self, id):
1✔
529
        """ Never allow renaming!
530
        """
531
        if id != self.getId():
1✔
532
            raise ValueError('Changing the id of this object is forbidden: %s'
1✔
533
                             % self.getId())
534

535

536
class UniqueObject(ImmutableId):
1✔
537

538
    """ Base class for objects which cannot be "overridden" / shadowed.
539
    """
540
    zmi_icon = 'fas fa-wrench'
1✔
541

542
    def _getUNIQUE(self):
1✔
543
        return UNIQUE
×
544

545
    __replaceable__ = property(_getUNIQUE)
1✔
546

547

548
class SimpleItemWithProperties(PropertyManager, SimpleItem):
1✔
549
    """
550
    A common base class for objects with configurable
551
    properties in a fixed schema.
552
    """
553
    manage_options = (
1✔
554
        PropertyManager.manage_options +
555
        SimpleItem.manage_options)
556

557
    security = ClassSecurityInfo()
1✔
558
    security.declarePrivate('manage_addProperty')  # NOQA: flake8: D001
1✔
559
    security.declarePrivate('manage_delProperties')  # NOQA: flake8: D001
1✔
560
    security.declarePrivate('manage_changePropertyTypes')  # NOQA: flake8: D001
1✔
561

562
    def manage_propertiesForm(self, REQUEST, *args, **kw):
1✔
563
        """ An override that makes the schema fixed.
564
        """
565
        my_kw = kw.copy()
×
566
        my_kw['property_extensible_schema__'] = 0
×
567
        form = PropertyManager.manage_propertiesForm.__of__(self)
×
568
        return form(self, REQUEST, *args, **my_kw)
×
569

570

571
InitializeClass(SimpleItemWithProperties)
1✔
572

573

574
#
575
#   "Omnibus" factory framework for tools.
576
#
577
class ToolInit:
1✔
578

579
    """ Utility class for generating the factories for several tools.
580
    """
581
    __name__ = 'toolinit'
1✔
582

583
    security = ClassSecurityInfo()
1✔
584
    security.declareObjectPrivate()     # equivalent of __roles__ = ()
1✔
585

586
    def __init__(self, meta_type, tools, product_name=None, icon=None):
1✔
587
        self.meta_type = meta_type
1✔
588
        self.tools = tools
1✔
589
        if product_name is not None:
1!
590
            warn('The product_name parameter of ToolInit is now ignored',
×
591
                 DeprecationWarning, stacklevel=2)
592
        self.icon = icon
1✔
593

594
    def initialize(self, context):
1✔
595
        # Add only one meta type to the folder add list.
596
        productObject = context._ProductContext__prod
1✔
597
        self.product_name = productObject.id
1✔
598
        # We add self to the FactoryDispatcher under the name 'toolinit'.
599
        # manage_addContentType() can then grab it.
600
        try:
1✔
601
            context.registerClass(
1✔
602
                meta_type=self.meta_type,
603
                constructors=(manage_addToolForm, manage_addTool),
604
                resources=(self, ),
605
                icon=self.icon)
606
        except TypeError:
×
607
            # The 'resources' keyword was only introduced after Zope 5.9.
608
            context.registerClass(
×
609
                meta_type=self.meta_type,
610
                constructors=(manage_addToolForm, manage_addTool, self),
611
                icon=self.icon)
612

613
        if self.icon:
1!
614
            icon = os_path.split(self.icon)[1]
1✔
615
        else:
616
            icon = None
×
617
        for tool in self.tools:
1✔
618
            tool.__factory_meta_type__ = self.meta_type
1✔
619
            tool.icon = f'misc_/{self.product_name}/{icon}'
1✔
620

621

622
InitializeClass(ToolInit)
1✔
623

624
addInstanceForm = HTMLFile('dtml/addInstance', globals())
1✔
625

626

627
def manage_addToolForm(self, REQUEST):
1✔
628
    """ Show the add tool form.
629
    """
630
    # self is a FactoryDispatcher.
631
    toolinit = self.toolinit
×
632
    tl = []
×
633
    for tool in toolinit.tools:
×
634
        tl.append(tool.meta_type)
×
635
    return addInstanceForm(addInstanceForm, self, REQUEST,
×
636
                           factory_action='manage_addTool',
637
                           factory_meta_type=toolinit.meta_type,
638
                           factory_product_name=toolinit.product_name,
639
                           factory_icon=toolinit.icon,
640
                           factory_types_list=tl,
641
                           factory_need_id=0)
642

643

644
def manage_addTool(self, type, REQUEST=None):
1✔
645
    """ Add the tool specified by name.
646
    """
647
    # self is a FactoryDispatcher.
648
    toolinit = self.toolinit
×
649
    obj = None
×
650
    for tool in toolinit.tools:
×
651
        if tool.meta_type == type:
×
652
            obj = tool()
×
653
            break
×
654
    if obj is None:
×
655
        raise NotFound(type)
×
656
    self._setObject(obj.getId(), obj)
×
657
    if REQUEST is not None:
×
658
        return self.manage_main(self, REQUEST)
×
659

660

661
#
662
#   Now, do the same for creating content factories.
663
#
664
class ContentInit:
1✔
665

666
    """ Utility class for generating factories for several content types.
667
    """
668
    __name__ = 'contentinit'
1✔
669

670
    security = ClassSecurityInfo()
1✔
671
    security.declareObjectPrivate()
1✔
672

673
    def __init__(self, meta_type, content_types, permission=None,
1✔
674
                 extra_constructors=(), fti=(), visibility='Global'):
675
        # BBB: fti argument is ignored
676
        self.meta_type = meta_type
1✔
677
        self.content_types = content_types
1✔
678
        self.permission = permission
1✔
679
        self.extra_constructors = extra_constructors
1✔
680
        self.visibility = visibility
1✔
681

682
    def initialize(self, context):
1✔
683
        # Add only one meta type to the folder add list.
684
        # We add self to the FactoryDispatcher under the name 'contentinit'.
685
        # manage_addContentType() can then grab it.
686
        try:
1✔
687
            context.registerClass(
1✔
688
                meta_type=self.meta_type,
689
                constructors=(
690
                    manage_addContentForm,
691
                    manage_addContent,
692
                ) + self.extra_constructors,
693
                resources=(self, ),
694
                permission=self.permission,
695
                visibility=self.visibility)
696
        except TypeError:
×
697
            # The 'resources' keyword was only introduced after Zope 5.9.
698
            context.registerClass(
×
699
                meta_type=self.meta_type,
700
                constructors=(
701
                    manage_addContentForm, manage_addContent, self,
702
                ) + self.extra_constructors,
703
                permission=self.permission,
704
                visibility=self.visibility)
705

706
        for ct in self.content_types:
1!
707
            ct.__factory_meta_type__ = self.meta_type
×
708

709

710
InitializeClass(ContentInit)
1✔
711

712

713
def manage_addContentForm(self, REQUEST):
1✔
714
    """ Show the add content type form.
715
    """
716
    # self is a FactoryDispatcher.
717
    ci = self.contentinit
×
718
    tl = []
×
719
    for t in ci.content_types:
×
720
        tl.append(t.meta_type)
×
721
    return addInstanceForm(addInstanceForm, self, REQUEST,
×
722
                           factory_action='manage_addContent',
723
                           factory_meta_type=ci.meta_type,
724
                           factory_icon=None,
725
                           factory_types_list=tl,
726
                           factory_need_id=1)
727

728

729
def manage_addContent(self, id, type, REQUEST=None):
1✔
730
    """ Add the content type specified by name.
731
    """
732
    # self is a FactoryDispatcher.
733
    contentinit = self.contentinit
×
734
    obj = None
×
735
    for content_type in contentinit.content_types:
×
736
        if content_type.meta_type == type:
×
737
            obj = content_type(id)
×
738
            break
×
739
    if obj is None:
×
740
        raise NotFound(type)
×
741
    self._setObject(id, obj)
×
742
    if REQUEST is not None:
×
743
        return self.manage_main(self, REQUEST)
×
744

745

746
def registerIcon(klass, iconspec, _prefix=None):
1✔
747
    """ Make an icon available for a given class.
748

749
    o 'klass' is the class being decorated.
750

751
    o 'iconspec' is the path within the product where the icon lives.
752
    """
753
    modname = klass.__module__
1✔
754
    pid = modname.split('.')[1]
1✔
755
    name = os_path.split(iconspec)[1]
1✔
756
    klass.icon = f'misc_/{pid}/{name}'
1✔
757
    icon = ImageFile(iconspec, _prefix)
1✔
758
    icon.__roles__ = None
1✔
759
    if not hasattr(misc_images, pid):
1✔
760
        setattr(misc_images, pid, MiscImage(pid, {}))
1✔
761
    getattr(misc_images, pid)[name] = icon
1✔
762

763

764
#
765
#   Metadata Keyword splitter utilities
766
#
767
KEYSPLITRE = re.compile(r'[,;]')
1✔
768

769

770
@security.public
1✔
771
def keywordsplitter(headers, names=('Subject', 'Keywords'),
1✔
772
                    splitter=KEYSPLITRE.split):
773
    """ Split keywords out of headers, keyed on names.  Returns list.
774
    """
775
    out = []
1✔
776
    for head in names:
1✔
777
        keylist = splitter(headers.get(head, ''))
1✔
778
        keylist = [x.strip() for x in keylist]
1✔
779
        out.extend([key for key in keylist if key])
1✔
780
    return out
1✔
781

782

783
#
784
#   Metadata Contributors splitter utilities
785
#
786
CONTRIBSPLITRE = re.compile(r';')
1✔
787

788

789
@security.public
1✔
790
def contributorsplitter(headers, names=('Contributors',),
1✔
791
                        splitter=CONTRIBSPLITRE.split):
792
    """ Split contributors out of headers, keyed on names.  Returns list.
793
    """
794
    return keywordsplitter(headers, names, splitter)
1✔
795

796

797
#
798
#   Directory-handling utilities
799
#
800
@security.public
1✔
801
def normalize(p):
1✔
802
    # the first .replace is needed to help normpath when dealing with Windows
803
    # paths under *nix, the second to normalize to '/'
804
    return os_path.normpath(p.replace('\\', '/')).replace('\\', '/')
1✔
805

806

807
@security.private
1✔
808
def getContainingPackage(module):
1✔
809
    parts = module.split('.')
1✔
810
    while parts:
1!
811
        name = '.'.join(parts)
1✔
812
        mod = sys.modules[name]
1✔
813
        if '__init__' in mod.__file__:
1✔
814
            return name
1✔
815
        parts = parts[:-1]
1✔
816

817
    raise ValueError('Unable to find package for module %s' % module)
×
818

819

820
@security.private
1✔
821
def getPackageLocation(module):
1✔
822
    """ Return the filesystem location of a module.
823

824
    This is a simple wrapper around the global package_home method which
825
    tricks it into working with just a module name.
826
    """
827
    package = getContainingPackage(module)
1✔
828
    return package_home({'__name__': package})
1✔
829

830

831
@security.private
1✔
832
def getPackageName(globals_):
1✔
833
    module = globals_['__name__']
1✔
834
    return getContainingPackage(module)
1✔
835

836

837
def _OldCacheHeaders(obj):
1✔
838
    # Old-style checking of modified headers
839

840
    REQUEST = getattr(obj, 'REQUEST', None)
1✔
841
    if REQUEST is None:
1!
842
        return False
×
843

844
    RESPONSE = REQUEST.RESPONSE
1✔
845
    header = REQUEST.getHeader('If-Modified-Since', None)
1✔
846
    last_mod = int(obj.modified().timeTime())
1✔
847

848
    if header is not None:
1!
849
        header = header.split(';')[0]
1✔
850
        # Some proxies seem to send invalid date strings for this
851
        # header. If the date string is not valid, we ignore it
852
        # rather than raise an error to be generally consistent
853
        # with common servers such as Apache (which can usually
854
        # understand the screwy date string as a lucky side effect
855
        # of the way they parse it).
856
        try:
1✔
857
            mod_since = DateTime(header)
1✔
858
            mod_since = int(mod_since.timeTime())
1✔
859
        except (TypeError, DateTimeError):
1✔
860
            mod_since = None
1✔
861

862
        if mod_since is not None:
1✔
863
            if last_mod > 0 and last_mod <= mod_since:
1✔
864
                RESPONSE.setStatus(304)
1✔
865
                return True
1✔
866

867
    # Last-Modified will get stomped on by a cache policy if there is
868
    # one set....
869
    RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
1✔
870

871

872
def _FSCacheHeaders(obj):
1✔
873
    # Old-style setting of modified headers for FS-based objects
874

875
    REQUEST = getattr(obj, 'REQUEST', None)
1✔
876
    if REQUEST is None:
1!
877
        return False
×
878

879
    RESPONSE = REQUEST.RESPONSE
1✔
880
    header = REQUEST.getHeader('If-Modified-Since', None)
1✔
881
    # Reduce resolution to one second, else if-modified-since would
882
    # always be older if system resolution is higher
883
    last_mod = int(obj._file_mod_time)
1✔
884

885
    if header is not None:
1✔
886
        header = header.split(';')[0]
1✔
887
        # Some proxies seem to send invalid date strings for this
888
        # header. If the date string is not valid, we ignore it
889
        # rather than raise an error to be generally consistent
890
        # with common servers such as Apache (which can usually
891
        # understand the screwy date string as a lucky side effect
892
        # of the way they parse it).
893
        try:
1✔
894
            mod_since = DateTime(header)
1✔
895
            mod_since = int(mod_since.timeTime())
1✔
896
        except (TypeError, DateTimeError):
1✔
897
            mod_since = None
1✔
898

899
        if mod_since is not None:
1✔
900
            if last_mod > 0 and last_mod <= mod_since:
1✔
901
                RESPONSE.setStatus(304)
1✔
902
                return True
1✔
903

904
    # Last-Modified will get stomped on by a cache policy if there is
905
    # one set....
906
    RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
1✔
907

908

909
class SimpleRecord:
1✔
910
    """ record-like class """
911

912
    def __init__(self, **kw):
1✔
913
        self.__dict__.update(kw)
×
914

915

916
def base64_encode(text):
1✔
917
    return base64.encodebytes(text).rstrip()
1✔
918

919

920
def base64_decode(text):
1✔
921
    return base64.decodebytes(text)
1✔
922

923

924
security.declarePublic('Message')  # NOQA: flake8: D001
1✔
925
Message = MessageFactory('cmf_core')
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