diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index d4eb9ec7ec451..6ef57c2f77733 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -5,7 +5,7 @@
from odoo import http, _
from odoo.addons.auth_signup.models.res_users import SignupError
-from odoo.addons.web.controllers.main import ensure_db, Home
+from odoo.addons.web.controllers.main import ensure_db, Home, SIGN_UP_REQUEST_PARAMS
from odoo.addons.base_setup.controllers.main import BaseSetup
from odoo.exceptions import UserError
from odoo.http import request
@@ -101,7 +101,7 @@ def get_auth_signup_config(self):
def get_auth_signup_qcontext(self):
""" Shared helper returning the rendering context for signup and reset password """
- qcontext = request.params.copy()
+ qcontext = {k: v for (k, v) in request.params.items() if k in SIGN_UP_REQUEST_PARAMS}
qcontext.update(self.get_auth_signup_config())
if not qcontext.get('token') and request.session.get('auth_signup_token'):
qcontext['token'] = request.session.get('auth_signup_token')
diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index 0d02fb084c414..ae8a6635e46ac 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -16,7 +16,7 @@
slugify_lib = None
import odoo
-from odoo import api, models, registry, exceptions, tools
+from odoo import api, models, registry, exceptions, tools, http
from odoo.addons.base.models.ir_http import RequestUID, ModelConverter
from odoo.addons.base.models.qweb import QWebException
from odoo.http import request
@@ -653,8 +653,7 @@ def _handle_exception(cls, exception):
@tools.ormcache('path')
def url_rewrite(self, path):
new_url = False
- req = request.httprequest
- router = req.app.get_db_router(request.db).bind('')
+ router = http.root.get_db_router(request.db).bind('')
try:
_ = router.match(path, method='POST')
except werkzeug.exceptions.MethodNotAllowed:
@@ -672,7 +671,7 @@ def url_rewrite(self, path):
@api.model
@tools.cache('path', 'query_args')
def _get_endpoint_qargs(self, path, query_args=None):
- router = request.httprequest.app.get_db_router(request.db).bind('')
+ router = http.root.get_db_router(request.db).bind('')
endpoint = False
try:
endpoint = router.match(path, method='POST', query_args=query_args)
diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py
index d80c07d57ec62..db7be2b388224 100644
--- a/addons/l10n_fr_fec/wizard/account_fr_fec.py
+++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py
@@ -7,7 +7,7 @@
import io
from odoo import api, fields, models, _
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, AccessDenied
from odoo.tools import float_is_zero, pycompat
@@ -30,7 +30,7 @@ def _onchange_export_file(self):
if not self.test_file:
self.export_type = 'official'
- def do_query_unaffected_earnings(self):
+ def _do_query_unaffected_earnings(self):
''' Compute the sum of ending balances for all accounts that are of a type that does not bring forward the balance in new fiscal years.
This is needed because we have to display only one line for the initial balance of all expense/revenue accounts in the FEC.
'''
@@ -103,6 +103,8 @@ def _get_company_legal_data(self, company):
def generate_fec(self):
self.ensure_one()
+ if not (self.env.is_admin() or self.env.user.has_group('account.group_account_user')):
+ raise AccessDenied()
# We choose to implement the flat file instead of the XML
# file for 2 reasons :
# 1) the XSD file impose to have the label on the account.move
@@ -147,7 +149,7 @@ def generate_fec(self):
unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once
if unaffected_earnings_xml_ref:
#compute the benefit/loss of last year to add in the initial balance of the current year earnings account
- unaffected_earnings_results = self.do_query_unaffected_earnings()
+ unaffected_earnings_results = self._do_query_unaffected_earnings()
unaffected_earnings_line = False
sql_query = '''
diff --git a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
index a42ede1c6ae6b..79bedaecbadba 100644
--- a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
+++ b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
@@ -750,7 +750,9 @@ function factory(dependencies) {
notificationTitle = owl.utils.escape(authorName);
}
}
- const notificationContent = htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE);
+ const notificationContent = owl.utils.escape(
+ htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE)
+ );
this.env.services['bus_service'].sendNotification(notificationTitle, notificationContent);
messaging.update({ outOfFocusUnreadMessageCounter: increment() });
const titlePattern = messaging.outOfFocusUnreadMessageCounter === 1
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index fdbb1e80a1813..725ee27e3a107 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1041,12 +1041,12 @@ def _create_payment_transaction(self, vals):
if payment_token and payment_token.acquirer_id != acquirer:
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
payment_token.acquirer_id.name, acquirer.name))
- if payment_token and payment_token.partner_id != partner:
- raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
- payment_token.partner.name, partner.name))
else:
acquirer = payment_token.acquirer_id
+ if payment_token and payment_token.partner_id != partner:
+ raise ValidationError(_('Invalid token found!'))
+
# Check an acquirer is there.
if not acquirer_id and not acquirer:
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index ab9168c8c6795..1f75df6045e11 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -128,9 +128,8 @@ def redirect_with_hash(*args, **kw):
return http.redirect_with_hash(*args, **kw)
def abort_and_redirect(url):
- r = request.httprequest
response = werkzeug.utils.redirect(url, 302)
- response = r.app.get_response(r, response, explicit_session=False)
+ response = http.root.get_response(request.httprequest, response, explicit_session=False)
werkzeug.exceptions.abort(response)
def ensure_db(redirect='/web/database/selector'):
@@ -647,6 +646,10 @@ def get_qweb_templates_checksum(cls, addons, db=None, debug=False):
def get_qweb_templates(cls, addons, db=None, debug=False):
return cls(addons, db, debug=debug)._get_qweb_templates()[0]
+# Shared parameters for all login/signup flows
+SIGN_UP_REQUEST_PARAMS = {'db', 'login', 'debug', 'token', 'message', 'error', 'scope', 'mode',
+ 'redirect', 'redirect_hostname', 'email', 'name', 'partner_id',
+ 'password', 'confirm_password', 'city', 'country_id', 'lang'}
class GroupsTreeNode:
"""
@@ -934,7 +937,7 @@ def web_login(self, redirect=None, **kw):
if not request.uid:
request.uid = odoo.SUPERUSER_ID
- values = request.params.copy()
+ values = {k: v for k, v in request.params.items() if k in SIGN_UP_REQUEST_PARAMS}
try:
values['databases'] = http.db_list()
except odoo.exceptions.AccessDenied:
@@ -1102,7 +1105,7 @@ def post(self, path):
from werkzeug.wrappers import BaseResponse
base_url = request.httprequest.base_url
query_string = request.httprequest.query_string
- client = Client(request.httprequest.app, BaseResponse)
+ client = Client(http.root, BaseResponse)
headers = {'X-Openerp-Session-Id': request.session.sid}
return client.post('/' + path, base_url=base_url, query_string=query_string,
headers=headers, data=data)
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d80bb9337fb11..112b5f75dfe31 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -2088,22 +2088,13 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
this.filename_value = this.recordData[this.attrs.filename];
},
_renderReadonly: function () {
- this.do_toggle(!!this.value);
- if (this.value) {
- this.$el.empty().append($("").addClass('fa fa-download'));
- if (this.recordData.id) {
- this.$el.css('cursor', 'pointer');
- } else {
- this.$el.css('cursor', 'not-allowed');
- }
- if (this.filename_value) {
- this.$el.append(" " + this.filename_value);
- }
- }
- if (!this.res_id) {
- this.$el.css('cursor', 'not-allowed');
- } else {
- this.$el.css('cursor', 'pointer');
+ var visible = !!(this.value && this.res_id);
+ this.$el.empty().css('cursor', 'not-allowed');
+ this.do_toggle(visible);
+ if (visible) {
+ this.$el.css('cursor', 'pointer')
+ .text(this.filename_value || '')
+ .prepend($(''), ' ');
}
},
_renderEdit: function () {
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 4ccdd292b4a72..d478a5bcdb837 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2441,7 +2441,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('binary fields that are readonly in create mode do not download', async function (assert) {
- assert.expect(2);
+ assert.expect(4);
// save the session function
var oldGetFile = session.get_file;
@@ -2473,10 +2473,17 @@ QUnit.module('basic_fields', {
await testUtils.fields.many2one.clickOpenDropdown('product_id');
await testUtils.fields.many2one.clickHighlightedItem('product_id');
- assert.containsOnce(form, 'a.o_field_widget[name="document"] > .fa-download',
+ assert.containsOnce(form, 'a.o_field_widget[name="document"]',
'The link to download the binary should be present');
+ assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download',
+ 'The download icon should not be present');
+
+ var link = form.$('a.o_field_widget[name="document"]');
+ assert.ok(link.is(':hidden'), "the link element should not be visible");
- testUtils.dom.click(form.$('a.o_field_widget[name="document"]'));
+ // force visibility to test that the clicking has also been disabled
+ link.removeClass('o_hidden');
+ testUtils.dom.click(link);
assert.verifySteps([]); // We shouldn't have passed through steps
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 54719351b989d..e4f485f4a77f6 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -12,7 +12,7 @@
from werkzeug.datastructures import OrderedMultiDict
from werkzeug.exceptions import NotFound
-from odoo import api, fields, models, tools
+from odoo import api, fields, models, tools, http
from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype, url_for
from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.portal.controllers.portal import pager
@@ -794,7 +794,7 @@ def _enumerate_pages(self, query_string=None, force=False):
:rtype: list({name: str, url: str})
"""
- router = request.httprequest.app.get_db_router(request.db)
+ router = http.root.get_db_router(request.db)
# Force enumeration to be performed as public user
url_set = set()
@@ -973,7 +973,7 @@ def _get_canonical_url_localized(self, lang, canonical_params):
"""
self.ensure_one()
if request.endpoint:
- router = request.httprequest.app.get_db_router(request.db).bind('')
+ router = http.root.get_db_router(request.db).bind('')
arguments = dict(request.endpoint_arguments)
for key, val in list(arguments.items()):
if isinstance(val, models.BaseModel):
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index b4ab28dfd4e42..f765e1bb96293 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -608,7 +608,10 @@ registry.mediaVideo = publicWidget.Widget.extend({
var def = this._super.apply(this, arguments);
if (this.$target.children('iframe').length) {
- // There already is an , do nothing
+ // There already is an , do nothing. This is the normal
+ // case. The whole code that follows is only there to ensure
+ // compatibility with videos added before bug fixes or new Odoo
+ // versions where the element is properly saved.
return def;
}
@@ -626,11 +629,23 @@ registry.mediaVideo = publicWidget.Widget.extend({
// the src is saved in the 'data-src' attribute or the
// 'data-oe-expression' one (the latter is used as a workaround in 10.0
// system but should obviously be reviewed in master).
+ var src = _.escape(this.$target.data('oe-expression') || this.$target.data('src'));
+ // Validate the src to only accept supported domains we can trust
+ var m = src.match(/^(?:https?:)?\/\/([^/?#]+)/);
+ if (!m) {
+ // Unsupported protocol or wrong URL format, don't inject iframe
+ return def;
+ }
+ var domain = m[1].replace(/^www\./, '');
+ var supportedDomains = ['youtu.be', 'youtube.com', 'youtube-nocookie.com', 'instagram.com', 'vine.co', 'player.vimeo.com', 'vimeo.com', 'dailymotion.com', 'player.youku.com', 'youku.com'];
+ if (!_.contains(supportedDomains, domain)) {
+ // Unsupported domain, don't inject iframe
+ return def;
+ }
this.$target.append($('', {
- src: _.escape(this.$target.data('oe-expression') || this.$target.data('src')),
+ src: src,
frameborder: '0',
allowfullscreen: 'allowfullscreen',
- sandbox: 'allow-scripts allow-same-origin', // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
}));
return def;
diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py
index 17fdc299a21c5..ea6f290c5b15d 100644
--- a/odoo/addons/base/models/assetsbundle.py
+++ b/odoo/addons/base/models/assetsbundle.py
@@ -13,11 +13,13 @@
# If the `sass` python library isn't found, we fallback on the
# `sassc` executable in the path.
libsass = None
+from contextlib import closing
from datetime import datetime
from subprocess import Popen, PIPE
from collections import OrderedDict
from odoo import fields, tools, SUPERUSER_ID
from odoo.tools.pycompat import to_text
+from odoo.tools.misc import file_open
from odoo.http import request
from odoo.modules.module import get_resource_path
from .qweb import escape
@@ -27,6 +29,8 @@
import logging
_logger = logging.getLogger(__name__)
+EXTENSIONS = (".js", ".css", ".scss", ".sass", ".less")
+
class CompileError(RuntimeError): pass
def rjsmin(script):
@@ -676,7 +680,7 @@ def _fetch_content(self):
try:
self.stat()
if self._filename:
- with open(self._filename, 'rb') as fp:
+ with closing(file_open(self._filename, 'rb', filter_ext=EXTENSIONS)) as fp:
return fp.read().decode('utf-8')
else:
return base64.b64decode(self._ir_attach['datas']).decode('utf-8')
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index b5d68d3305497..73cca6b16b612 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -728,13 +728,13 @@ def _render_qweb_pdf(self, res_ids=None, data=None):
data = {}
data.setdefault('report_type', 'pdf')
- # access the report details with sudo() but evaluation context as current user
+ # access the report details with sudo() but evaluation context as sudo(False)
self_sudo = self.sudo()
# In case of test environment without enough workers to perform calls to wkhtmltopdf,
# fallback to render_html.
if (tools.config['test_enable'] or tools.config['test_file']) and not self.env.context.get('force_report_rendering'):
- return self._render_qweb_html(res_ids, data=data)
+ return self_sudo._render_qweb_html(res_ids, data=data)
# As the assets are generated during the same transaction as the rendering of the
# templates calling them, there is a scenario where the assets are unreachable: when
@@ -831,7 +831,7 @@ def _render_qweb_text(self, docids, data=None):
data = {}
data.setdefault('report_type', 'text')
data = self._get_rendering_context(docids, data)
- return self._render_template(self.sudo().report_name, data), 'text'
+ return self._render_template(self.report_name, data), 'text'
@api.model
def _render_qweb_html(self, docids, data=None):
@@ -841,29 +841,29 @@ def _render_qweb_html(self, docids, data=None):
data = {}
data.setdefault('report_type', 'html')
data = self._get_rendering_context(docids, data)
- return self._render_template(self.sudo().report_name, data), 'html'
+ return self._render_template(self.report_name, data), 'html'
def _get_rendering_context_model(self):
report_model_name = 'report.%s' % self.report_name
return self.env.get(report_model_name)
def _get_rendering_context(self, docids, data):
- # access the report details with sudo() but evaluation context as current user
- self_sudo = self.sudo()
-
# If the report is using a custom model to render its html, we must use it.
# Otherwise, fallback on the generic html rendering.
- report_model = self_sudo._get_rendering_context_model()
+ report_model = self._get_rendering_context_model()
data = data and dict(data) or {}
if report_model is not None:
+ # _render_ may be executed in sudo but evaluation context as real user
+ report_model = report_model.sudo(False)
data.update(report_model._get_report_values(docids, data=data))
else:
- docs = self.env[self_sudo.model].browse(docids)
+ # _render_ may be executed in sudo but evaluation context as real user
+ docs = self.env[self.model].sudo(False).browse(docids)
data.update({
'doc_ids': docids,
- 'doc_model': self_sudo.model,
+ 'doc_model': self.model,
'docs': docs,
})
return data
diff --git a/odoo/addons/base/models/ir_demo.py b/odoo/addons/base/models/ir_demo.py
index 6c8775aee5e94..e7ee63874efd6 100644
--- a/odoo/addons/base/models/ir_demo.py
+++ b/odoo/addons/base/models/ir_demo.py
@@ -1,5 +1,9 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from odoo import models
from odoo.modules.loading import force_demo
+from odoo.addons.base.models.ir_module import assert_log_admin_access
class IrDemo(models.TransientModel):
@@ -7,6 +11,7 @@ class IrDemo(models.TransientModel):
_name = 'ir.demo'
_description = 'Demo'
+ @assert_log_admin_access
def install_demo(self):
force_demo(self.env.cr)
return {
diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py
index aac796b46303d..c385ade9d95ea 100644
--- a/odoo/addons/base/models/ir_module.py
+++ b/odoo/addons/base/models/ir_module.py
@@ -66,7 +66,7 @@ def assert_log_admin_access(method):
def check_and_log(method, self, *args, **kwargs):
user = self.env.user
origin = request.httprequest.remote_addr if request else 'n/a'
- log_data = (method.__name__, self.sudo().mapped('name'), user.login, user.id, origin)
+ log_data = (method.__name__, self.sudo().mapped('display_name'), user.login, user.id, origin)
if not self.env.is_admin():
_logger.warning('DENY access to module.%s on %s to user %s ID #%s via %s', *log_data)
raise AccessDenied()
diff --git a/odoo/http.py b/odoo/http.py
index 7e605a27561f1..bef907a8388ba 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -1427,7 +1427,6 @@ def dispatch(self, environ, start_response):
"""
try:
httprequest = werkzeug.wrappers.Request(environ)
- httprequest.app = self
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
current_thread = threading.current_thread()
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index 2473a80a29a9a..7b6874249f1d7 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -103,6 +103,7 @@ def new(cls, db_name, force_demo=False, status=None, update_module=False):
registry._init = False
registry.ready = True
registry.registry_invalidated = bool(update_module)
+ registry.new = registry.init = registry.registries = None
return registry
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 8aa037d08775c..1cc657c38ea23 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -138,7 +138,7 @@ def exec_pg_command_pipe(name, *args):
#file_path_root = os.getcwd()
#file_path_addons = os.path.join(file_path_root, 'addons')
-def file_open(name, mode="r", subdir='addons', pathinfo=False):
+def file_open(name, mode="r", subdir='addons', pathinfo=False, filter_ext=None):
"""Open a file from the OpenERP root, using a subdir folder.
Example::
@@ -150,6 +150,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
@param mode file open mode
@param subdir subdirectory
@param pathinfo if True returns tuple (fileobject, filepath)
+ @param filter_ext: optional list of supported extensions (without leading dot)
@return fileobject if pathinfo is False else (fileobject, filepath)
"""
@@ -171,7 +172,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
else:
# It is outside the OpenERP root: skip zipfile lookup.
base, name = os.path.split(name)
- return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename)
+ return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename, filter_ext=filter_ext)
if name.replace(os.sep, '/').startswith('addons/'):
subdir = 'addons'
@@ -189,15 +190,15 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
for adp in adps:
try:
return _fileopen(name2, mode=mode, basedir=adp,
- pathinfo=pathinfo, basename=basename)
+ pathinfo=pathinfo, basename=basename, filter_ext=filter_ext)
except IOError:
pass
# Second, try to locate in root_path
- return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
+ return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename, filter_ext=filter_ext)
-def _fileopen(path, mode, basedir, pathinfo, basename=None):
+def _fileopen(path, mode, basedir, pathinfo, basename=None, filter_ext=None):
name = os.path.normpath(os.path.normcase(os.path.join(basedir, path)))
paths = odoo.addons.__path__ + [config['root_path']]
@@ -208,6 +209,9 @@ def _fileopen(path, mode, basedir, pathinfo, basename=None):
else:
raise ValueError("Unknown path: %s" % name)
+ if filter_ext and not name.lower().endswith(filter_ext):
+ raise ValueError("Unsupported path: %s" % name)
+
if basename is None:
basename = name
# Give higher priority to module directories, which is