Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
erp5
Commits
50c48dbd
Commit
50c48dbd
authored
Jan 29, 2020
by
Vincent Pelletier
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
ERP5Type.mixin.ResponseHeaderGenerator: New class.
Make ERP5Type.Base and ERP5.ERP5Site inherit from it.
parent
133d6655
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
368 additions
and
2 deletions
+368
-2
bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testERP5Base.py
...tTemplateItem/portal_components/test.erp5.testERP5Base.py
+122
-1
product/ERP5/ERP5Site.py
product/ERP5/ERP5Site.py
+2
-1
product/ERP5Type/Base.py
product/ERP5Type/Base.py
+2
-0
product/ERP5Type/mixin/response_header_generator.py
product/ERP5Type/mixin/response_header_generator.py
+242
-0
No files found.
bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testERP5Base.py
View file @
50c48dbd
...
...
@@ -26,7 +26,7 @@
#
##############################################################################
from
collections
import
defaultdict
import
os
import
unittest
...
...
@@ -1663,6 +1663,127 @@ class TestERP5Base(ERP5TypeTestCase):
self
.
tic
()
self
.
assertEqual
(
chat_address
.
getId
(),
chat_address_id
)
def
test_response_header_generator
(
self
):
portal
=
self
.
portal
person_module
=
portal
.
person_module
response_header_dict
=
defaultdict
(
set
)
def
setResponseHeaderRule
(
document
,
header_name
,
method_id
=
None
,
fallback_value
=
''
,
fallback_value_replace
=
False
,
):
document
.
setResponseHeaderRule
(
header_name
,
method_id
,
fallback_value
,
fallback_value_replace
,
)
self
.
commit
()
# document.setResponseHeaderRule succeeded, flag for cleanup
response_header_dict
[
document
].
add
(
header_name
)
def
assertPublishedHeaderEqual
(
document
,
header_name
,
value
):
self
.
assertEqual
(
self
.
publish
(
document
.
getPath
()).
getHeader
(
header_name
),
value
,
)
try
:
# Invalid header names are rejected
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
' '
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
':'
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'
\
t
'
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'
\
r
'
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'
\
n
'
)
# Invalid header values are rejected
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'Foo'
,
fallback_value
=
'
\
x7f
'
,
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'Foo'
,
fallback_value
=
'
\
x1f
'
,
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'Foo'
,
fallback_value
=
'
\
r
'
,
)
self
.
assertRaises
(
ValueError
,
setResponseHeaderRule
,
portal
,
'Foo'
,
fallback_value
=
'
\
n
'
,
)
# Test sanity checks
# Nothing succeeded, cleanup must still be empty.
assert
not
response_header_dict
header_name
=
'Bar'
value
=
'this is a value'
script_value
=
'this comes from the script'
other_value
=
'this is another value'
script_container_value
=
self
.
getSkinsTool
().
custom
script_argument_string
=
(
'request, header_name, fallback_value, fallback_value_replace, '
'current_value'
)
script_id
=
'ERP5Site_getBarResponseHeader'
createZODBPythonScript
(
script_container_value
,
script_id
,
script_argument_string
,
'return %r, False'
%
(
script_value
,
),
)
raising_script_id
=
'ERP5Site_getBarResponseHeaderRaising'
createZODBPythonScript
(
script_container_value
,
raising_script_id
,
script_argument_string
,
'raise Exception'
,
)
bad_value_script_id
=
'ERP5Site_getBadBarResponseHeader'
createZODBPythonScript
(
script_container_value
,
bad_value_script_id
,
script_argument_string
,
'return "
\
\
n", False'
,
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
None
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
None
)
# Basic functionality: fallback only
setResponseHeaderRule
(
portal
,
header_name
,
fallback_value
=
value
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
value
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
value
)
# Basic functionality: dynamic invalid value
setResponseHeaderRule
(
portal
,
header_name
,
method_id
=
bad_value_script_id
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
None
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
None
)
# Basic functionality: dynamic value with fallback
setResponseHeaderRule
(
portal
,
header_name
,
method_id
=
raising_script_id
,
fallback_value
=
value
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
value
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
value
)
# Basic functionality: dynamic value
setResponseHeaderRule
(
portal
,
header_name
,
method_id
=
script_id
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
script_value
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
script_value
)
# Value overriding
setResponseHeaderRule
(
person_module
,
header_name
,
fallback_value
=
other_value
,
fallback_value_replace
=
True
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
script_value
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
other_value
)
# Already-set value is appended to
setResponseHeaderRule
(
person_module
,
header_name
,
fallback_value
=
other_value
,
fallback_value_replace
=
False
)
assertPublishedHeaderEqual
(
portal
,
header_name
,
script_value
)
assertPublishedHeaderEqual
(
person_module
,
header_name
,
script_value
+
', '
+
other_value
)
finally
:
for
document
,
header_name_set
in
response_header_dict
.
iteritems
():
for
header_name
in
header_name_set
:
try
:
document
.
deleteResponseHeaderRule
(
header_name
)
except
KeyError
:
pass
def
test_suite
():
suite
=
unittest
.
TestSuite
()
suite
.
addTest
(
unittest
.
makeSuite
(
TestERP5Base
))
...
...
product/ERP5/ERP5Site.py
View file @
50c48dbd
...
...
@@ -38,6 +38,7 @@ from Products.CMFActivity.Errors import ActivityPendingError
import
ERP5Defaults
from
Products.ERP5Type.TransactionalVariable
import
getTransactionalVariable
from
Products.ERP5Type.dynamic.portal_type_class
import
synchronizeDynamicModules
from
Products.ERP5Type.mixin.response_header_generator
import
ResponseHeaderGenerator
from
zLOG
import
LOG
,
INFO
,
WARNING
,
ERROR
from
string
import
join
...
...
@@ -227,7 +228,7 @@ class _site(threading.local):
getSite
,
setSite
=
_site
()
class
ERP5Site
(
FolderMixIn
,
CMFSite
,
CacheCookieMixin
):
class
ERP5Site
(
ResponseHeaderGenerator
,
FolderMixIn
,
CMFSite
,
CacheCookieMixin
):
"""
The *only* function this class should have is to help in the setup
of a new ERP5. It should not assist in the functionality at all.
...
...
product/ERP5Type/Base.py
View file @
50c48dbd
...
...
@@ -88,6 +88,7 @@ from Products.ERP5Type.Message import Message
from
Products.ERP5Type.ConsistencyMessage
import
ConsistencyMessage
from
Products.ERP5Type.UnrestrictedMethod
import
UnrestrictedMethod
,
super_user
from
Products.ERP5Type.mixin.json_representable
import
JSONRepresentableMixin
from
Products.ERP5Type.mixin.response_header_generator
import
ResponseHeaderGenerator
from
zope.interface
import
classImplementsOnly
,
implementedBy
...
...
@@ -707,6 +708,7 @@ def initializePortalTypeDynamicWorkflowMethods(ptype_klass, portal_workflow):
method
.
registerTransitionAlways
(
portal_type
,
wf_id
,
tr_id
)
class
Base
(
ResponseHeaderGenerator
,
CopyContainer
,
PropertyManager
,
PortalContent
,
...
...
product/ERP5Type/mixin/response_header_generator.py
0 → 100644
View file @
50c48dbd
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2020 Nexedi SA and Contributors. All Rights Reserved.
# Vincent Pelletier <vincent@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from
itertools
import
chain
from
AccessControl
import
ClassSecurityInfo
import
ExtensionClass
from
Products.ERP5Type
import
Permissions
from
Products.ERP5Type.Globals
import
InitializeClass
from
Products.ERP5Type.Globals
import
PersistentMapping
from
zLOG
import
LOG
,
ERROR
def
_makeForbiddenCharList
(
*
args
):
result
=
[
True
]
*
256
for
char
in
chain
(
*
args
):
result
[
char
]
=
False
return
tuple
(
result
)
# https://tools.ietf.org/html/rfc7230#section-3.2
IS_FORBIDDEN_HEADER_NAME_CHAR_LIST
=
_makeForbiddenCharList
(
(
ord
(
x
)
for
x
in
"!#$%&'*+-.^_`|~"
),
xrange
(
0x30
,
0x3a
),
# DIGIT
xrange
(
0x61
,
0x7b
),
# ALPHA, only lower-case
)
# Note: RFC defines field_value as not starting with SP nor HTAB,
# but this is because these are stripped during parsing. Allow
# them during generation.
IS_FORBIDDEN_HEADER_VALUE_CHAR_LIST
=
_makeForbiddenCharList
(
[
0x09
],
# HTAB
xrange
(
0x20
,
0x7f
),
# SP + VCHAR
xrange
(
0x80
,
0x100
),
# obs-text
)
del
_makeForbiddenCharList
class
ResponseHeaderGenerator
(
ExtensionClass
.
Base
):
"""
Mix-in class allowing instances of its host class to define response
headers of any request traversing it.
For example, allows setting site-wide headers, and then overriding some
when a WebSite document is traversed in the same request.
Note that this happens on traversal (aka "document ID is in the URL"), and
not on any other access.
"""
security
=
ClassSecurityInfo
()
# We create a new security info object
security
.
declareProtected
(
Permissions
.
ManagePortal
,
'getResponseHeaderRuleDict'
)
def
getResponseHeaderRuleDict
(
self
):
"""
Return a mapping describing currently-defined response header rules.
Modifying returned value does not have any effect on stored rules (use
setResponseHeaderRule & deleteResponseHaderRule).
Key (str)
Header name.
Valid character set (as per rfc7230):
"!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
"^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
DIGIT being range: 0x30-0x39
ALPHA being limited to range: 0x61-0x7A (lower-case only)
Value (dict)
"method_id" (string)
Identifier of a callable accessible on self.
If empty, fallback_value and fallback_value_replace will always be
used.
Parameters (passed by name):
request (BaseRequest) Current request object.
header_name (str) (see above)
fallback_value (str) (see below)
fallback_value_replace (bool) (see below)
current_value (str, None)
The value of this header in current response.
None if it is not set yet.
Return value (tuple)
[0]: Header value (str) (see fallback_value below)
[1]: Replace (bool) (see fallback_value_replace below)
Such callable should refrain from accessing the response directly.
"fallback_value" (str)
Header value to use if given method is unusable (raises or
inaccessible).
Valid characted set (as per rfc7230): HTAB, 0x20-0x7E, 0x80-0xFF
"fallback_value_replace" (bool)
When true, fallback_value replaces any pre-existing value.
If fallback_value is empty, this removes the header from the response.
When false, fallback_value is appended to any pre-existing value,
separated with ", ".
If fallback_value is empty, this response header is left unchanged.
"""
return
{
header_name
:
{
'method_id'
:
method_id
,
'fallback_value'
:
fallback_value
,
'fallback_value_replace'
:
fallback_value_replace
,
}
for
(
header_name
,
(
method_id
,
fallback_value
,
fallback_value_replace
)
)
in
getattr
(
self
,
'_response_header_rule_dict'
,
{}).
iteritems
()
}
def
_getResponseHeaderRuleDictForModification
(
self
):
"""
Retrieve persistent rule dict storage.
Use only when a modification is requested, to avoid creating useless
subobjects.
"""
try
:
return
self
.
_response_header_rule_dict
except
AttributeError
:
self
.
_response_header_rule_dict
=
rule_dict
=
PersistentMapping
()
return
rule_dict
security
.
declareProtected
(
Permissions
.
ManagePortal
,
'setResponseHeaderRule'
)
def
setResponseHeaderRule
(
self
,
header_name
,
method_id
,
fallback_value
,
fallback_value_replace
,
):
"""
Create or modify a header rule.
See getResponseHeaderRuleDict for a parameter description.
header_name is lower-cased before validation and storage.
"""
header_name
=
header_name
.
lower
()
if
not
header_name
:
raise
ValueError
(
'Header name must not be empty'
)
for
char
in
header_name
:
if
IS_FORBIDDEN_HEADER_NAME_CHAR_LIST
[
ord
(
char
)]:
raise
ValueError
(
'%r is not a valid header name character'
%
(
char
,
),
)
for
char
in
fallback_value
:
if
IS_FORBIDDEN_HEADER_VALUE_CHAR_LIST
[
ord
(
char
)]:
raise
ValueError
(
'%r is not a valid header value character'
%
(
char
,
),
)
self
.
_getResponseHeaderRuleDictForModification
()[
header_name
]
=
(
method_id
,
fallback_value
,
bool
(
fallback_value_replace
),
)
security
.
declareProtected
(
Permissions
.
ManagePortal
,
'deleteResponseHeaderRule'
)
def
deleteResponseHeaderRule
(
self
,
header_name
):
"""
Delete an existing header rule.
"""
del
self
.
_getResponseHeaderRuleDictForModification
()[
header_name
]
def
__before_publishing_traverse__
(
self
,
self2
,
request
):
try
:
response
=
request
.
RESPONSE
setHeader
=
response
.
setHeader
appendHeader
=
response
.
appendHeader
removeHeader
=
response
.
headers
.
pop
except
AttributeError
:
# Response does not support setting headers, nothing to do.
pass
else
:
for
(
header_name
,
(
method_id
,
value
,
value_replace
)
)
in
getattr
(
self
,
'_response_header_rule_dict'
,
{}).
iteritems
():
if
method_id
:
try
:
method_value
=
getattr
(
self
,
method_id
)
except
AttributeError
:
LOG
(
__name__
,
ERROR
,
'Cannot access %r.%r to generate response header %r, using fallback value'
%
(
self
,
method_id
,
header_name
,
),
)
else
:
fallback_value
=
value
fallback_value_replace
=
value_replace
try
:
value
,
value_replace
=
method_value
(
request
=
request
,
header_name
=
header_name
,
fallback_value
=
value
,
fallback_value_replace
=
value_replace
,
current_value
=
response
.
getHeader
(
header_name
),
)
for
char
in
value
:
if
IS_FORBIDDEN_HEADER_VALUE_CHAR_LIST
[
ord
(
char
)]:
value
=
fallback_value
value_replace
=
fallback_value_replace
raise
ValueError
(
'%r is not a valid header value character'
%
(
char
,
),
)
except
Exception
:
LOG
(
__name__
,
ERROR
,
'%r.%r raised when generating response header %r, using fallback value'
%
(
self
,
method_id
,
header_name
,
),
error
=
True
,
)
if
value
:
(
setHeader
if
value_replace
else
appendHeader
)(
header_name
,
value
)
elif
value_replace
:
removeHeader
(
header_name
)
# else, no value and append: nothing to do.
return
super
(
ResponseHeaderGenerator
,
self
,
).
__before_publishing_traverse__
(
self2
,
request
)
InitializeClass
(
ResponseHeaderGenerator
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment