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
Mikolaï Krol
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