Commit 5224c417 authored by Rafael Monnerat's avatar Rafael Monnerat

Update from upstream/master

parents a4c33bde dc9ffa12
...@@ -808,7 +808,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase): ...@@ -808,7 +808,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
preference = self.portal.portal_catalog.getResultValue( preference = self.portal.portal_catalog.getResultValue(
portal_type='System Preference', portal_type='System Preference',
title='Authentication',) title='Authentication',)
# Here we activate the "password should contain usename" policy # Here we activate the "password should contain username" policy
# as a way to check that password reset checks are done in the # as a way to check that password reset checks are done in the
# context of the login # context of the login
preference.setPrefferedForceUsernameCheckInPassword(1) preference.setPrefferedForceUsernameCheckInPassword(1)
...@@ -856,8 +856,11 @@ class TestAuthenticationPolicy(ERP5TypeTestCase): ...@@ -856,8 +856,11 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
# now with a password complying to the policy # now with a password complying to the policy
ret = submit_reset_password_dialog('ok') ret = submit_reset_password_dialog('ok')
self.assertEqual(httplib.FOUND, ret.getStatus()) self.assertEqual(httplib.FOUND, ret.getStatus())
self.assertTrue(ret.getHeader('Location').endswith( redirect_url = urlparse.urlparse(ret.getHeader("Location"))
'/login_form?portal_status_message=Password+changed.')) self.assertEqual(redirect_url.path, '{}/login_form'.format(self.portal.absolute_url_path()))
redirect_url_params = urlparse.parse_qsl(redirect_url.query)
self.assertIn(('portal_status_message', 'Password changed.'), redirect_url_params)
self.assertIn(('portal_status_level', 'success'), redirect_url_params)
def test_PreferenceTool_changePassword_checks_policy(self): def test_PreferenceTool_changePassword_checks_policy(self):
person = self.createUser(self.id(), password='current') person = self.createUser(self.id(), password='current')
...@@ -918,7 +921,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase): ...@@ -918,7 +921,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
# long enough password is accepted # long enough password is accepted
ret = submit_change_password_dialog('long_enough_password') ret = submit_change_password_dialog('long_enough_password')
# When password reset is succesful, user is logged out # When password reset is successful, user is logged out
self.assertEqual(httplib.FOUND, ret.getStatus()) self.assertEqual(httplib.FOUND, ret.getStatus())
self.assertEqual(self.portal.portal_preferences.absolute_url(), self.assertEqual(self.portal.portal_preferences.absolute_url(),
ret.getHeader("Location")) ret.getHeader("Location"))
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</item> </item>
<item> <item>
<key> <string>cache_duration</string> </key> <key> <string>cache_duration</string> </key>
<value> <int>86400</int> </value> <value> <int>864000</int> </value>
</item> </item>
<item> <item>
<key> <string>description</string> </key> <key> <string>description</string> </key>
......
...@@ -513,6 +513,8 @@ var GameManager = /** @class */ (function () { ...@@ -513,6 +513,8 @@ var GameManager = /** @class */ (function () {
function GameManager(canvas, game_parameters_json) { function GameManager(canvas, game_parameters_json) {
var drone, header_list; var drone, header_list;
this._canvas = canvas; this._canvas = canvas;
this._canvas_width = canvas.width;
this._canvas_height = canvas.height;
this._scene = null; this._scene = null;
this._engine = null; this._engine = null;
this._droneList = []; this._droneList = [];
...@@ -575,7 +577,7 @@ var GameManager = /** @class */ (function () { ...@@ -575,7 +577,7 @@ var GameManager = /** @class */ (function () {
}); });
}; };
GameManager.prototype.update = function () { GameManager.prototype.update = function (fullscreen) {
// time delta means that drone are updated every virtual second // time delta means that drone are updated every virtual second
// This is fixed and must not be modified // This is fixed and must not be modified
// otherwise, it will lead to different scenario results // otherwise, it will lead to different scenario results
...@@ -587,10 +589,8 @@ var GameManager = /** @class */ (function () { ...@@ -587,10 +589,8 @@ var GameManager = /** @class */ (function () {
function triggerUpdateIfPossible() { function triggerUpdateIfPossible() {
if ((_this._canUpdate) && (_this.ongoing_update_promise === null) && if ((_this._canUpdate) && (_this.ongoing_update_promise === null) &&
(0 < _this.waiting_update_count)) { (0 < _this.waiting_update_count)) {
_this.ongoing_update_promise = _this._update( _this.ongoing_update_promise = _this._update(TIME_DELTA, fullscreen)
TIME_DELTA, .push(function () {
(_this.waiting_update_count === 1)
).push(function () {
_this.waiting_update_count -= 1; _this.waiting_update_count -= 1;
_this.ongoing_update_promise = null; _this.ongoing_update_promise = null;
triggerUpdateIfPossible(); triggerUpdateIfPossible();
...@@ -626,7 +626,7 @@ var GameManager = /** @class */ (function () { ...@@ -626,7 +626,7 @@ var GameManager = /** @class */ (function () {
return false; return false;
}; };
GameManager.prototype._update = function (delta_time) { GameManager.prototype._update = function (delta_time, fullscreen) {
var _this = this, var _this = this,
queue = new RSVP.Queue(), queue = new RSVP.Queue(),
i; i;
...@@ -642,6 +642,20 @@ var GameManager = /** @class */ (function () { ...@@ -642,6 +642,20 @@ var GameManager = /** @class */ (function () {
} }
} }
if (fullscreen) {
//Only resize if size changes
if (this._canvas.width !== GAMEPARAMETERS.fullscreen.width) {
this._canvas.width = GAMEPARAMETERS.fullscreen.width;
this._canvas.height = GAMEPARAMETERS.fullscreen.height;
}
} else {
if (this._canvas.width !== this._canvas_width) {
this._canvas.width = this._canvas_width;
this._canvas.height = this._canvas_height;
this._engine.resize(true);
}
}
this._droneList.forEach(function (drone) { this._droneList.forEach(function (drone) {
queue.push(function () { queue.push(function () {
drone._tick += 1; drone._tick += 1;
...@@ -1043,9 +1057,9 @@ var runGame, updateGame; ...@@ -1043,9 +1057,9 @@ var runGame, updateGame;
return game_manager_instance.run(); return game_manager_instance.run();
}; };
updateGame = function () { updateGame = function (fullscreen) {
if (game_manager_instance) { if (game_manager_instance) {
return game_manager_instance.update(); return game_manager_instance.update(fullscreen);
} }
}; };
......
...@@ -226,7 +226,7 @@ ...@@ -226,7 +226,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <unicode>zope</unicode> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -240,7 +240,7 @@ ...@@ -240,7 +240,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>1006.43905.28804.11980</string> </value> <value> <string>1009.7345.31305.44339</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -260,7 +260,7 @@ ...@@ -260,7 +260,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1677600104.11</float> <float>1687455790.77</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</item> </item>
<item> <item>
<key> <string>cache_duration</string> </key> <key> <string>cache_duration</string> </key>
<value> <int>86400</int> </value> <value> <int>864000</int> </value>
</item> </item>
<item> <item>
<key> <string>description</string> </key> <key> <string>description</string> </key>
......
history_list = context.getMovementHistoryList(**kw)
reverse_list = []
for x in history_list:
reverse_list.insert(0, x)
return reverse_list
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>**kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Resource_getReversedMovementHistoryList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -561,7 +561,7 @@ ...@@ -561,7 +561,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>method_name</string> </key> <key> <string>method_name</string> </key>
<value> <string>getMovementHistoryList</string> </value> <value> <string>Resource_getReversedMovementHistoryList</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
...@@ -16,14 +16,40 @@ ...@@ -16,14 +16,40 @@
<key> <string>content_icon</string> </key> <key> <string>content_icon</string> </key>
<value> <string>folder_icon.gif</string> </value> <value> <string>folder_icon.gif</string> </value>
</item> </item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item> <item>
<key> <string>factory</string> </key> <key> <string>factory</string> </key>
<value> <string>addFolder</string> </value> <value> <string>addFolder</string> </value>
</item> </item>
<item>
<key> <string>group_list</string> </key>
<value>
<tuple>
<string>module</string>
</tuple>
</value>
</item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>Delivery Node Module</string> </value> <value> <string>Delivery Node Module</string> </value>
</item> </item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item> <item>
<key> <string>portal_type</string> </key> <key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value> <value> <string>Base Type</string> </value>
...@@ -44,6 +70,18 @@ ...@@ -44,6 +70,18 @@
<key> <string>type_group</string> </key> <key> <string>type_group</string> </key>
<value> <string>module</string> </value> <value> <string>module</string> </value>
</item> </item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>type_mixin</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary> </dictionary>
</pickle> </pickle>
</record> </record>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
""" """
REQUEST = context.REQUEST REQUEST = context.REQUEST
next_url = context.portal_password.changeUserPassword(password=REQUEST['password'], next_url = context.portal_password.changeUserPassword(password=REQUEST['password'],
password_confirmation=REQUEST['password_confirm'], password_confirm=REQUEST['password_confirm'],
password_key=REQUEST['password_key'], password_key=REQUEST['password_key'],
user_login=REQUEST.get('user_login', None), user_login=REQUEST.get('user_login', None),
REQUEST=REQUEST) REQUEST=REQUEST)
......
...@@ -117,6 +117,7 @@ class TestStaticWebSiteRedirection(ERP5TypeTestCase): ...@@ -117,6 +117,7 @@ class TestStaticWebSiteRedirection(ERP5TypeTestCase):
connection = httplib.HTTPSConnection(netloc_to_check, context=ssl._create_unverified_context(), timeout=10) connection = httplib.HTTPSConnection(netloc_to_check, context=ssl._create_unverified_context(), timeout=10)
else: else:
connection = httplib.HTTPConnection(netloc_to_check, timeout=10) connection = httplib.HTTPConnection(netloc_to_check, timeout=10)
self.addCleanup(connection.close)
connection.request( connection.request(
method="GET", method="GET",
url=url_to_check url=url_to_check
......
...@@ -2603,7 +2603,7 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -2603,7 +2603,7 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
self.assertEqual(activity_node, current_node) self.assertEqual(activity_node, current_node)
def test_getServerAddress(self): def test_getServerAddress(self):
host, port = self.startZServer() host, port = self.startHTTPServer()
ip = socket.gethostbyname(host) ip = socket.gethostbyname(host)
server_address = '%s:%s' % (ip, port) server_address = '%s:%s' % (ip, port)
address = getServerAddress() address = getServerAddress()
......
...@@ -164,7 +164,7 @@ class Alarm(XMLObject, PeriodicityMixin): ...@@ -164,7 +164,7 @@ class Alarm(XMLObject, PeriodicityMixin):
activate_kw['tag'] = '%s_%x' % (self.getRelativeUrl(), getrandbits(32)) activate_kw['tag'] = '%s_%x' % (self.getRelativeUrl(), getrandbits(32))
tag = activate_kw['tag'] tag = activate_kw['tag']
method = getattr(self, method_id) method = getattr(self, method_id)
func_code = method.__code__ func_code = getattr(method, '__code__', None)
if func_code is None: # BBB Zope2 if func_code is None: # BBB Zope2
func_code = method.func_code func_code = method.func_code
try: try:
......
...@@ -31,6 +31,7 @@ from AccessControl import ClassSecurityInfo ...@@ -31,6 +31,7 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type import Permissions, PropertySheet
from App.special_dtml import HTMLFile from App.special_dtml import HTMLFile
from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type import IS_ZOPE2
from Products.PythonScripts.PythonScript import \ from Products.PythonScripts.PythonScript import \
PythonScript as ZopePythonScript PythonScript as ZopePythonScript
from Products.ERP5Type.mixin.expression import ExpressionMixin from Products.ERP5Type.mixin.expression import ExpressionMixin
...@@ -71,6 +72,8 @@ class PythonScript(XMLObject, ZopePythonScript, ExpressionMixin('expression')): ...@@ -71,6 +72,8 @@ class PythonScript(XMLObject, ZopePythonScript, ExpressionMixin('expression')):
meta_type = 'ERP5 Python Script' meta_type = 'ERP5 Python Script'
portal_type = 'Python Script' portal_type = 'Python Script'
add_permission = Permissions.AddPortalContent add_permission = Permissions.AddPortalContent
if not IS_ZOPE2:
zmi_icon = ZopePythonScript.zmi_icon
# Declarative security # Declarative security
security = ClassSecurityInfo() security = ClassSecurityInfo()
......
REQUEST = context.REQUEST REQUEST = context.REQUEST
return context.portal_password.changeUserPassword(password=REQUEST['password'], return context.portal_password.changeUserPassword(password=REQUEST['password'],
password_confirmation=REQUEST['password_confirm'], password_confirm=REQUEST['password_confirm'],
password_key=REQUEST['password_key'], password_key=REQUEST['password_key'],
user_login=REQUEST.get('user_login', None), user_login=REQUEST.get('user_login', None),
REQUEST=REQUEST) REQUEST=REQUEST)
...@@ -41,9 +41,9 @@ from BTrees.OOBTree import OOBTree ...@@ -41,9 +41,9 @@ from BTrees.OOBTree import OOBTree
from six.moves.urllib.parse import urlencode from six.moves.urllib.parse import urlencode
redirect_path = '/login_form' redirect_path = '/login_form'
def redirect(REQUEST, site_url, message): def redirect(REQUEST, site_url, message, level):
if REQUEST is not None and getattr(REQUEST.RESPONSE, 'redirect', None) is not None: if REQUEST is not None and getattr(REQUEST.RESPONSE, 'redirect', None) is not None:
parameter = urlencode({'portal_status_message': message}) parameter = urlencode({'portal_status_message': message, 'portal_status_level': level})
ret_url = '%s%s?%s' % (site_url, redirect_path, parameter) ret_url = '%s%s?%s' % (site_url, redirect_path, parameter)
return REQUEST.RESPONSE.redirect( ret_url ) return REQUEST.RESPONSE.redirect( ret_url )
else: else:
...@@ -171,10 +171,13 @@ class PasswordTool(BaseTool): ...@@ -171,10 +171,13 @@ class PasswordTool(BaseTool):
"User {user} does not have a valid email address".format(user=user_login) "User {user} does not have a valid email address".format(user=user_login)
) )
if error_encountered: if error_encountered:
# note that we intentionally use the same msg here regardless of whether the
# email was successfully sent or not in order not to leak information about user
# existence.
if batch: if batch:
raise RuntimeError(msg) raise RuntimeError(msg)
else: else:
return redirect(REQUEST, site_url, msg) return redirect(REQUEST, site_url, msg, 'success')
key = self.getResetPasswordKey(user_login=user_login, key = self.getResetPasswordKey(user_login=user_login,
expiration_date=expiration_date) expiration_date=expiration_date)
...@@ -222,8 +225,7 @@ class PasswordTool(BaseTool): ...@@ -222,8 +225,7 @@ class PasswordTool(BaseTool):
message_text_format=message_text_format, message_text_format=message_text_format,
event_keyword_argument_dict=event_keyword_argument_dict) event_keyword_argument_dict=event_keyword_argument_dict)
if not batch: if not batch:
return redirect(REQUEST, site_url, return redirect(REQUEST, site_url, msg, 'success')
translateString("An email has been sent to you."))
security.declareProtected(Permissions.ModifyPortalContent, 'removeExpiredRequests') security.declareProtected(Permissions.ModifyPortalContent, 'removeExpiredRequests')
def removeExpiredRequests(self): def removeExpiredRequests(self):
...@@ -266,13 +268,12 @@ class PasswordTool(BaseTool): ...@@ -266,13 +268,12 @@ class PasswordTool(BaseTool):
""" """
Reset the password for a given login Reset the password for a given login
""" """
# BBB: password_confirm: unused argument
def error(message): def error(message):
# BBB: should "raise Redirect" instead of just returning, simplifying # BBB: should "raise Redirect" instead of just returning, simplifying
# calling code and making mistakes more difficult # calling code and making mistakes more difficult
# BBB: should probably not translate message when REQUEST is None # BBB: should probably not translate message when REQUEST is None
message = translateString(message) message = translateString(message)
return redirect(REQUEST, site_url, message) return redirect(REQUEST, site_url, message, 'error')
if REQUEST is None: if REQUEST is None:
REQUEST = get_request() REQUEST = get_request()
...@@ -291,6 +292,8 @@ class PasswordTool(BaseTool): ...@@ -291,6 +292,8 @@ class PasswordTool(BaseTool):
if user_login is not None and register_user_login != user_login: if user_login is not None and register_user_login != user_login:
# XXX: not descriptive enough # XXX: not descriptive enough
return error("Bad login provided.") return error("Bad login provided.")
if password_confirm is not None and password_confirm != password:
return error("Password does not match the confirm password.")
if DateTime() > expiration_date: if DateTime() > expiration_date:
return error("Date has expired.") return error("Date has expired.")
del self._password_request_dict[password_key] del self._password_request_dict[password_key]
...@@ -303,6 +306,6 @@ class PasswordTool(BaseTool): ...@@ -303,6 +306,6 @@ class PasswordTool(BaseTool):
login = portal.unrestrictedTraverse(login_dict['path']) login = portal.unrestrictedTraverse(login_dict['path'])
login.setPassword(password) # this will raise if password does not match policy login.setPassword(password) # this will raise if password does not match policy
return redirect(REQUEST, site_url, return redirect(REQUEST, site_url,
translateString("Password changed.")) translateString("Password changed."), 'success')
InitializeClass(PasswordTool) InitializeClass(PasswordTool)
...@@ -151,7 +151,7 @@ def addERP5KeyAuthPlugin(dispatcher, id, title=None, ...@@ -151,7 +151,7 @@ def addERP5KeyAuthPlugin(dispatcher, id, title=None,
class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper): class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper):
""" """
Key authentification PAS plugin which support key authentication in URL. Key authentication PAS plugin which support key authentication in URL.
<ERP5_Root>/web_page_module/1?__ac_key=207221200213146153166 <ERP5_Root>/web_page_module/1?__ac_key=207221200213146153166
...@@ -309,7 +309,7 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper): ...@@ -309,7 +309,7 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper):
################################ ################################
security.declarePrivate('resetCredentials') security.declarePrivate('resetCredentials')
def resetCredentials(self, request, response): def resetCredentials(self, request, response):
"""Expire cookies of authentification """ """Expire cookies of authentication """
response.expireCookie(self.cookie_name, path='/') response.expireCookie(self.cookie_name, path='/')
response.expireCookie(self.default_cookie_name, path='/') response.expireCookie(self.default_cookie_name, path='/')
...@@ -319,7 +319,7 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper): ...@@ -319,7 +319,7 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper):
################################ ################################
security.declarePrivate('authenticateCredentials') security.declarePrivate('authenticateCredentials')
def authenticateCredentials( self, credentials ): def authenticateCredentials( self, credentials ):
"""Authentificate with credentials""" """Authenticate with credentials"""
key = credentials.get('key', None) key = credentials.get('key', None)
if key != None: if key != None:
login = self.decrypt(key) login = self.decrypt(key)
...@@ -377,9 +377,9 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper): ...@@ -377,9 +377,9 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper):
LOG('ERP5KeyAuthPlugin.authenticateCredentials', PROBLEM, str(e)) LOG('ERP5KeyAuthPlugin.authenticateCredentials', PROBLEM, str(e))
return None return None
################################ #################################
# Properties for ZMI managment # # Properties for ZMI management #
################################ #################################
#'Edit' option form #'Edit' option form
manage_editERP5KeyAuthPluginForm = PageTemplateFile( manage_editERP5KeyAuthPluginForm = PageTemplateFile(
...@@ -393,7 +393,7 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper): ...@@ -393,7 +393,7 @@ class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper):
"""Edit the object""" """Edit the object"""
error_message = '' error_message = ''
#Test paramaeters #Test parameters
if "__ac_key" in [cookie_name, default_cookie_name]: if "__ac_key" in [cookie_name, default_cookie_name]:
raise ValueError("Cookie name must be different of __ac_key") raise ValueError("Cookie name must be different of __ac_key")
......
...@@ -96,6 +96,8 @@ class Message(Persistent): ...@@ -96,6 +96,8 @@ class Message(Persistent):
def __init__(self, domain=None, message='', def __init__(self, domain=None, message='',
mapping=None, default=None): mapping=None, default=None):
self.message = message self.message = message
if mapping is not None:
assert isinstance(mapping, dict)
self.mapping = mapping self.mapping = mapping
self.domain = domain self.domain = domain
if default is None: if default is None:
......
...@@ -35,6 +35,7 @@ import sys ...@@ -35,6 +35,7 @@ import sys
import imp import imp
import collections import collections
from six import reraise from six import reraise
import traceback
import coverage import coverage
from Products.ERP5Type.Utils import ensure_list from Products.ERP5Type.Utils import ensure_list
...@@ -60,6 +61,13 @@ except NameError: # < 3.6 ...@@ -60,6 +61,13 @@ except NameError: # < 3.6
class ModuleNotFoundError(ImportError): class ModuleNotFoundError(ImportError):
pass pass
class ComponentImportError(ImportError):
"""Error when importing an existing, but invalid component, typically
because it contains syntax errors or import errors.
"""
class ComponentDynamicPackage(ModuleType): class ComponentDynamicPackage(ModuleType):
""" """
A top-level component is a package as it contains modules, this is required A top-level component is a package as it contains modules, this is required
...@@ -355,15 +363,17 @@ class ComponentDynamicPackage(ModuleType): ...@@ -355,15 +363,17 @@ class ComponentDynamicPackage(ModuleType):
# in a deadlock # in a deadlock
source_code_obj = compile(source_code_str, module.__file__, 'exec') source_code_obj = compile(source_code_str, module.__file__, 'exec')
exec(source_code_obj, module.__dict__) exec(source_code_obj, module.__dict__)
except Exception as error: except Exception:
del sys.modules[module_fullname] del sys.modules[module_fullname]
if module_fullname_alias: if module_fullname_alias:
del sys.modules[module_fullname_alias] del sys.modules[module_fullname_alias]
if module_fullname_filesystem: if module_fullname_filesystem:
del sys.modules[module_fullname_filesystem] del sys.modules[module_fullname_filesystem]
reraise(ImportError, reraise(
"%s: cannot load Component %s (%s)" % (fullname, name, error), ComponentImportError,
"%s: cannot load Component %s :\n%s" % (
fullname, name, traceback.format_exc()),
sys.exc_info()[2]) sys.exc_info()[2])
# Add the newly created module to the Version package and add it as an # Add the newly created module to the Version package and add it as an
......
...@@ -22,6 +22,7 @@ from OFS.misc_ import p_ ...@@ -22,6 +22,7 @@ from OFS.misc_ import p_
from App.ImageFile import ImageFile from App.ImageFile import ImageFile
from Acquisition import aq_base, aq_parent from Acquisition import aq_base, aq_parent
from zExceptions import Forbidden from zExceptions import Forbidden
from Products.ERP5Type import IS_ZOPE2
### Guards ### Guards
...@@ -153,6 +154,7 @@ class _(PatchClass(PythonScript)): ...@@ -153,6 +154,7 @@ class _(PatchClass(PythonScript)):
# Add proxy role icon in ZMI # Add proxy role icon in ZMI
if IS_ZOPE2:
def om_icons(self): def om_icons(self):
"""Return a list of icon URLs to be displayed by an ObjectManager""" """Return a list of icon URLs to be displayed by an ObjectManager"""
if self._proxy_roles: if self._proxy_roles:
...@@ -164,7 +166,13 @@ class _(PatchClass(PythonScript)): ...@@ -164,7 +166,13 @@ class _(PatchClass(PythonScript)):
p_.PythonScript_ProxyRole_icon = \ p_.PythonScript_ProxyRole_icon = \
ImageFile('pyscript_proxyrole.gif', globals()) ImageFile('pyscript_proxyrole.gif', globals())
else:
@property
def zmi_icon(self):
if self._proxy_roles:
return 'fa fa-terminal fa-spin'
else:
return 'fa fa-terminal'
# Guards # Guards
......
...@@ -428,7 +428,7 @@ class ERP5TypeFunctionalTestCase(ERP5TypeTestCase): ...@@ -428,7 +428,7 @@ class ERP5TypeFunctionalTestCase(ERP5TypeTestCase):
# non-recursive results clean of portal_tests/ or portal_tests/``run_only`` # non-recursive results clean of portal_tests/ or portal_tests/``run_only``
self.portal.portal_tests.TestTool_cleanUpTestResults(self.run_only or None) self.portal.portal_tests.TestTool_cleanUpTestResults(self.run_only or None)
self.tic() self.tic()
host, port = self.startZServer() host, port = self.startHTTPServer()
self.runner = FunctionalTestRunner(host, port, self) self.runner = FunctionalTestRunner(host, port, self)
def setSystemPreference(self): def setSystemPreference(self):
......
...@@ -185,7 +185,7 @@ class ERP5TypeLiveTestCase(ERP5TypeTestCaseMixin): ...@@ -185,7 +185,7 @@ class ERP5TypeLiveTestCase(ERP5TypeTestCaseMixin):
finally: finally:
restoreInteraction() restoreInteraction()
from Products.ERP5Type.dynamic.component_package import ComponentDynamicPackage from Products.ERP5Type.dynamic.component_package import ComponentDynamicPackage, ComponentImportError
from Products.ERP5Type.tests.runUnitTest import ERP5TypeTestLoader from Products.ERP5Type.tests.runUnitTest import ERP5TypeTestLoader
class ERP5TypeTestReLoader(ERP5TypeTestLoader): class ERP5TypeTestReLoader(ERP5TypeTestLoader):
...@@ -221,6 +221,8 @@ class ERP5TypeTestReLoader(ERP5TypeTestLoader): ...@@ -221,6 +221,8 @@ class ERP5TypeTestReLoader(ERP5TypeTestLoader):
if module is None: if module is None:
try: try:
self._importZodbTestComponent(name.split('.')[0]) self._importZodbTestComponent(name.split('.')[0])
except ComponentImportError:
raise
except ImportError: except ImportError:
pass pass
else: else:
......
...@@ -1284,7 +1284,7 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin): ...@@ -1284,7 +1284,7 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
if len(setup_done) == 1: # make sure it is run only once if len(setup_done) == 1: # make sure it is run only once
self._setUpDummyMailHost() self._setUpDummyMailHost()
self.startZServer(verbose=True) self.startHTTPServer(verbose=True)
self._registerNode(distributing=1, processing=1) self._registerNode(distributing=1, processing=1)
self.loadPromise() self.loadPromise()
......
...@@ -151,9 +151,10 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase): ...@@ -151,9 +151,10 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase):
pass pass
Lifetime.graceful_shutdown_loop() Lifetime.graceful_shutdown_loop()
def startZServer(self, verbose=False): @staticmethod
"""Start HTTP ZServer in background""" def startHTTPServer(verbose=False):
if self._server_address is None: """Start HTTP Server in background"""
if ProcessingNodeTestCase._server_address is None:
from Products.ERP5Type.tests.runUnitTest import log_directory from Products.ERP5Type.tests.runUnitTest import log_directory
log = os.path.join(log_directory, "Z2.log") log = os.path.join(log_directory, "Z2.log")
message = "Running %s server at %s:%s\n" message = "Running %s server at %s:%s\n"
...@@ -199,8 +200,11 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase): ...@@ -199,8 +200,11 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase):
webdav_ports=webdav_ports), webdav_ports=webdav_ports),
logger, logger,
sockets=sockets) sockets=sockets)
ProcessingNodeTestCase._server = hs
ProcessingNodeTestCase._server_address = hs.addr ProcessingNodeTestCase._server_address = hs.addr
t = Thread(target=hs.run) ProcessingNodeTestCase._server_thread = t = Thread(
target=hs.run,
name='ProcessingNodeTestCase.startHTTPServer')
t.setDaemon(1) t.setDaemon(1)
t.start() t.start()
from Products.CMFActivity import ActivityTool from Products.CMFActivity import ActivityTool
...@@ -210,7 +214,15 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase): ...@@ -210,7 +214,15 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase):
if ActivityTool.currentNode == ActivityTool._server_address: if ActivityTool.currentNode == ActivityTool._server_address:
ActivityTool.currentNode = None ActivityTool.currentNode = None
ActivityTool._server_address = None ActivityTool._server_address = None
return self._server_address return ProcessingNodeTestCase._server_address
startZServer = startHTTPServer # BBB
@staticmethod
def stopHTTPServer():
if ProcessingNodeTestCase._server_address is not None:
ProcessingNodeTestCase._server_address = None
ProcessingNodeTestCase._server.close()
ProcessingNodeTestCase._server_thread.join(5)
def _registerNode(self, distributing, processing): def _registerNode(self, distributing, processing):
"""Register node to process and/or distribute activities""" """Register node to process and/or distribute activities"""
...@@ -338,7 +350,7 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase): ...@@ -338,7 +350,7 @@ class ProcessingNodeTestCase(ZopeTestCase.TestCase):
def afterSetUp(self): def afterSetUp(self):
"""Initialize a node that will only process activities""" """Initialize a node that will only process activities"""
self.startZServer() self.startHTTPServer()
# Make sure to still have possibilities to edit components # Make sure to still have possibilities to edit components
addUserToDeveloperRole('ERP5TypeTestCase') addUserToDeveloperRole('ERP5TypeTestCase')
from Zope2.custom_zodb import cluster from Zope2.custom_zodb import cluster
......
...@@ -40,10 +40,11 @@ if save_mysql: ...@@ -40,10 +40,11 @@ if save_mysql:
# The output of mysqldump needs to merge many lines at a time # The output of mysqldump needs to merge many lines at a time
# for performance reasons (merging lines is at most 10 times # for performance reasons (merging lines is at most 10 times
# faster, so this produce somewhat not nice to read sql # faster, so this produce somewhat not nice to read sql
command = 'mysqldump %s > %s' % (getMySQLArguments(), dump_sql_path,) command = 'mysqldump %s > %s.tmp' % (getMySQLArguments(), dump_sql_path,)
if verbosity: if verbosity:
_print('Dumping MySQL database with %s ...' % command) _print('Dumping MySQL database with %s ...' % command)
subprocess.check_call(command, shell=True) subprocess.check_call(command, shell=True)
os.rename(dump_sql_path + '.tmp', dump_sql_path)
if load: if load:
if save_mysql: if save_mysql:
......
...@@ -693,6 +693,7 @@ def runUnitTestList(test_list, verbosity=1, debug=0, run_only=None): ...@@ -693,6 +693,7 @@ def runUnitTestList(test_list, verbosity=1, debug=0, run_only=None):
raise raise
finally: finally:
ProcessingNodeTestCase.unregisterNode() ProcessingNodeTestCase.unregisterNode()
ProcessingNodeTestCase.stopHTTPServer()
db_factory.close() db_factory.close()
Storage.close() Storage.close()
if node_pid_list is not None: if node_pid_list is not None:
......
...@@ -3296,6 +3296,67 @@ class Test(ERP5TypeTestCase): ...@@ -3296,6 +3296,67 @@ class Test(ERP5TypeTestCase):
expected_msg_re = re.compile('Ran 3 test.*OK', re.DOTALL) expected_msg_re = re.compile('Ran 3 test.*OK', re.DOTALL)
self.assertRegex(output, expected_msg_re) self.assertRegex(output, expected_msg_re)
def testRunLiveTestImportError(self):
source_code = '''
def break_at_import():
import non.existing.module # pylint:disable=import-error
break_at_import()
'''
component = self._newComponent('testRunLiveTestImportError', source_code)
component.validate()
self.tic()
from Products.ERP5Type.tests.runUnitTest import ERP5TypeTestLoader
ERP5TypeTestLoader_loadTestsFromNames = ERP5TypeTestLoader.loadTestsFromNames
def loadTestsFromNames(self, *args, **kwargs):
"""
Monkey patched to simulate a reset right after importing the ZODB Test
Component whose Unit Tests are going to be executed
"""
ret = ERP5TypeTestLoader_loadTestsFromNames(self, *args, **kwargs)
from Products.ERP5.ERP5Site import getSite
getSite().portal_components.reset(force=True)
# Simulate a new REQUEST while the old one has been GC'ed
import erp5.component
erp5.component.ref_manager.clear()
import gc
gc.collect()
return ret
self.assertEqual(component.getValidationState(), 'validated')
self._component_tool.reset(force=True,
reset_portal_type_at_transaction_boundary=True)
def runLiveTest(test_name):
# ERP5TypeLiveTestCase.runLiveTest patches ERP5TypeTestCase bases, thus it
# needs to be restored after calling runLiveTest
base_tuple = ERP5TypeTestCase.__bases__
ERP5TypeTestLoader.loadTestsFromNames = loadTestsFromNames
try:
self._component_tool.runLiveTest(test_name)
finally:
ERP5TypeTestCase.__bases__ = base_tuple
ERP5TypeTestLoader.loadTestsFromNames = ERP5TypeTestLoader_loadTestsFromNames
return self._component_tool.readTestOutput()
output = runLiveTest('testRunLiveTestImportError')
self.assertIn('''
File "<portal_components/test.erp5.testRunLiveTestImportError>", line 4, in <module>
break_at_import()
File "<portal_components/test.erp5.testRunLiveTestImportError>", line 3, in break_at_import
import non.existing.module # pylint:disable=import-error
ImportError: No module named non.existing.module
''', output)
output = runLiveTest('testDoesNotExist_import_error_because_module_does_not_exist')
self.assertIn(
"ImportError: No module named testDoesNotExist_import_error_because_module_does_not_exist",
output)
def testERP5Broken(self): def testERP5Broken(self):
# Create a broken ghost object # Create a broken ghost object
import erp5.portal_type import erp5.portal_type
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
<dd> <dd>
The connection string used for Z MySQL Database Connection is of the form: The connection string used for Z MySQL Database Connection is of the form:
<br /> <br />
<code>[*lock] [+/-][database][@host[:port]] [user [password [unix_socket]]]</code> <code>[%ssl_name] [*lock] [+/-][database][@host[:port]] [user [password [unix_socket]]]</code>
<br /> <br />
or typically: or typically:
<br /> <br />
...@@ -73,6 +73,16 @@ ...@@ -73,6 +73,16 @@
If the UNIX socket is in a non-standard location, you can specify If the UNIX socket is in a non-standard location, you can specify
the full path to it after the password. the full path to it after the password.
</dd> </dd>
<dd>
%<em>ssl_name</em> at the begining of the connection string means to use
a ssl client certificate for authentication.
This will use a CA certificate located at
<code>$INSTANCEHOME/etc/zmysqlda/[%ssl_name]-ca.pem</code>, a client certificate
at <code>$INSTANCEHOME/etc/zmysqlda/[%ssl_name]-cert.pem</code> with a key
at <code>$INSTANCEHOME/etc/zmysqlda/[%ssl_name]-key.pem</code>.
This will also verify that the connection is using ssl and cause an error
when an encrypted connection can not be established.
</dd>
<dd> <dd>
A '-' in front of the database tells ZMySQLDA to not use Zope's A '-' in front of the database tells ZMySQLDA to not use Zope's
Transaction Manager, even if the server supports transactions. A Transaction Manager, even if the server supports transactions. A
......
...@@ -107,6 +107,7 @@ if _v < MySQLdb_version_required: ...@@ -107,6 +107,7 @@ if _v < MySQLdb_version_required:
from MySQLdb.converters import conversions from MySQLdb.converters import conversions
from MySQLdb.constants import FIELD_TYPE, CR, ER, CLIENT from MySQLdb.constants import FIELD_TYPE, CR, ER, CLIENT
from App.config import getConfiguration
from Shared.DC.ZRDB.TM import TM from Shared.DC.ZRDB.TM import TM
from DateTime import DateTime from DateTime import DateTime
from zLOG import LOG, ERROR, WARNING from zLOG import LOG, ERROR, WARNING
...@@ -115,7 +116,8 @@ from ZODB.POSException import ConflictError ...@@ -115,7 +116,8 @@ from ZODB.POSException import ConflictError
hosed_connection = ( hosed_connection = (
CR.SERVER_GONE_ERROR, CR.SERVER_GONE_ERROR,
CR.SERVER_LOST, CR.SERVER_LOST,
CR.COMMANDS_OUT_OF_SYNC CR.COMMANDS_OUT_OF_SYNC,
1927, # ER_CONNECTION_KILLED "Connection was killed" in MariaDB
) )
query_syntax_error = ( query_syntax_error = (
...@@ -245,6 +247,14 @@ class DB(TM): ...@@ -245,6 +247,14 @@ class DB(TM):
items = self._connection.split() items = self._connection.split()
if not items: if not items:
return return
if items[0][0] == "%":
cert_base_name = items.pop(0)[1:]
instancehome = getConfiguration().instancehome
kwargs['ssl'] = {
'ca': os.path.join(instancehome, 'etc', 'zmysqlda', cert_base_name + '-ca.pem'),
'cert': os.path.join(instancehome, 'etc', 'zmysqlda', cert_base_name + '-cert.pem'),
'key': os.path.join(instancehome, 'etc', 'zmysqlda', cert_base_name + '-key.pem'),
}
if items[0] == "~": if items[0] == "~":
kwargs['compress'] = True kwargs['compress'] = True
del items[0] del items[0]
...@@ -319,7 +329,12 @@ class DB(TM): ...@@ -319,7 +329,12 @@ class DB(TM):
error=True, error=True,
) )
self.db = MySQLdb.connect(**self._kw_args) self.db = MySQLdb.connect(**self._kw_args)
self._query("SET time_zone='+00:00'") self._query(b"SET time_zone='+00:00'")
# BBB mysqlclient on python2 does not support sql_mode, check that
# the connection is actually encrypted.
if self._kw_args.get('ssl') and \
not self._query(b"SHOW STATUS LIKE 'Ssl_version'").fetch_row()[0][1]:
raise NotSupportedError("Connection established without SSL")
def tables(self, rdb=0, def tables(self, rdb=0,
_care=('TABLE', 'VIEW')): _care=('TABLE', 'VIEW')):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment