219 lines
8.1 KiB
Python
219 lines
8.1 KiB
Python
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import copy
|
||
|
import hashlib
|
||
|
import io
|
||
|
import logging
|
||
|
import re
|
||
|
from collections import OrderedDict, defaultdict
|
||
|
|
||
|
import babel.messages.pofile
|
||
|
import werkzeug
|
||
|
import werkzeug.exceptions
|
||
|
import werkzeug.utils
|
||
|
import werkzeug.wrappers
|
||
|
import werkzeug.wsgi
|
||
|
from lxml import etree
|
||
|
from werkzeug.urls import iri_to_uri
|
||
|
|
||
|
from odoo.tools.translate import JAVASCRIPT_TRANSLATION_COMMENT, WEB_TRANSLATION_COMMENT
|
||
|
from odoo.tools.misc import file_open
|
||
|
from odoo import http
|
||
|
from odoo.http import request
|
||
|
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
def clean_action(action, env):
|
||
|
action_type = action.setdefault('type', 'ir.actions.act_window_close')
|
||
|
if action_type == 'ir.actions.act_window':
|
||
|
action = fix_view_modes(action)
|
||
|
|
||
|
# When returning an action, keep only relevant fields/properties
|
||
|
readable_fields = env[action['type']]._get_readable_fields()
|
||
|
action_type_fields = env[action['type']]._fields.keys()
|
||
|
|
||
|
cleaned_action = {
|
||
|
field: value
|
||
|
for field, value in action.items()
|
||
|
# keep allowed fields and custom properties fields
|
||
|
if field in readable_fields or field not in action_type_fields
|
||
|
}
|
||
|
|
||
|
# Warn about custom properties fields, because use is discouraged
|
||
|
action_name = action.get('name') or action
|
||
|
custom_properties = action.keys() - readable_fields - action_type_fields
|
||
|
if custom_properties:
|
||
|
_logger.warning("Action %r contains custom properties %s. Passing them "
|
||
|
"via the `params` or `context` properties is recommended instead",
|
||
|
action_name, ', '.join(map(repr, custom_properties)))
|
||
|
|
||
|
return cleaned_action
|
||
|
|
||
|
|
||
|
def ensure_db(redirect='/web/database/selector', db=None):
|
||
|
# This helper should be used in web client auth="none" routes
|
||
|
# if those routes needs a db to work with.
|
||
|
# If the heuristics does not find any database, then the users will be
|
||
|
# redirected to db selector or any url specified by `redirect` argument.
|
||
|
# If the db is taken out of a query parameter, it will be checked against
|
||
|
# `http.db_filter()` in order to ensure it's legit and thus avoid db
|
||
|
# forgering that could lead to xss attacks.
|
||
|
if db is None:
|
||
|
db = request.params.get('db') and request.params.get('db').strip()
|
||
|
|
||
|
# Ensure db is legit
|
||
|
if db and db not in http.db_filter([db]):
|
||
|
db = None
|
||
|
|
||
|
if db and not request.session.db:
|
||
|
# User asked a specific database on a new session.
|
||
|
# That mean the nodb router has been used to find the route
|
||
|
# Depending on installed module in the database, the rendering of the page
|
||
|
# may depend on data injected by the database route dispatcher.
|
||
|
# Thus, we redirect the user to the same page but with the session cookie set.
|
||
|
# This will force using the database route dispatcher...
|
||
|
r = request.httprequest
|
||
|
url_redirect = werkzeug.urls.url_parse(r.base_url)
|
||
|
if r.query_string:
|
||
|
# in P3, request.query_string is bytes, the rest is text, can't mix them
|
||
|
query_string = iri_to_uri(r.query_string)
|
||
|
url_redirect = url_redirect.replace(query=query_string)
|
||
|
request.session.db = db
|
||
|
werkzeug.exceptions.abort(request.redirect(url_redirect.to_url(), 302))
|
||
|
|
||
|
# if db not provided, use the session one
|
||
|
if not db and request.session.db and http.db_filter([request.session.db]):
|
||
|
db = request.session.db
|
||
|
|
||
|
# if no database provided and no database in session, use monodb
|
||
|
if not db:
|
||
|
all_dbs = http.db_list(force=True)
|
||
|
if len(all_dbs) == 1:
|
||
|
db = all_dbs[0]
|
||
|
|
||
|
# if no db can be found til here, send to the database selector
|
||
|
# the database selector will redirect to database manager if needed
|
||
|
if not db:
|
||
|
werkzeug.exceptions.abort(request.redirect(redirect, 303))
|
||
|
|
||
|
# always switch the session to the computed db
|
||
|
if db != request.session.db:
|
||
|
request.session = http.root.session_store.new()
|
||
|
request.session.update(http.get_default_session(), db=db)
|
||
|
request.session.context['lang'] = request.default_lang()
|
||
|
werkzeug.exceptions.abort(request.redirect(request.httprequest.url, 302))
|
||
|
|
||
|
|
||
|
def fix_view_modes(action):
|
||
|
""" For historical reasons, Odoo has weird dealings in relation to
|
||
|
view_mode and the view_type attribute (on window actions):
|
||
|
|
||
|
* one of the view modes is ``tree``, which stands for both list views
|
||
|
and tree views
|
||
|
* the choice is made by checking ``view_type``, which is either
|
||
|
``form`` for a list view or ``tree`` for an actual tree view
|
||
|
|
||
|
This methods simply folds the view_type into view_mode by adding a
|
||
|
new view mode ``list`` which is the result of the ``tree`` view_mode
|
||
|
in conjunction with the ``form`` view_type.
|
||
|
|
||
|
TODO: this should go into the doc, some kind of "peculiarities" section
|
||
|
|
||
|
:param dict action: an action descriptor
|
||
|
:returns: nothing, the action is modified in place
|
||
|
"""
|
||
|
if not action.get('views'):
|
||
|
generate_views(action)
|
||
|
|
||
|
if action.pop('view_type', 'form') != 'form':
|
||
|
return action
|
||
|
|
||
|
if 'view_mode' in action:
|
||
|
action['view_mode'] = ','.join(
|
||
|
mode if mode != 'tree' else 'list'
|
||
|
for mode in action['view_mode'].split(','))
|
||
|
action['views'] = [
|
||
|
[id, mode if mode != 'tree' else 'list']
|
||
|
for id, mode in action['views']
|
||
|
]
|
||
|
|
||
|
return action
|
||
|
|
||
|
|
||
|
# I think generate_views,fix_view_modes should go into js ActionManager
|
||
|
def generate_views(action):
|
||
|
"""
|
||
|
While the server generates a sequence called "views" computing dependencies
|
||
|
between a bunch of stuff for views coming directly from the database
|
||
|
(the ``ir.actions.act_window model``), it's also possible for e.g. buttons
|
||
|
to return custom view dictionaries generated on the fly.
|
||
|
|
||
|
In that case, there is no ``views`` key available on the action.
|
||
|
|
||
|
Since the web client relies on ``action['views']``, generate it here from
|
||
|
``view_mode`` and ``view_id``.
|
||
|
|
||
|
Currently handles two different cases:
|
||
|
|
||
|
* no view_id, multiple view_mode
|
||
|
* single view_id, single view_mode
|
||
|
|
||
|
:param dict action: action descriptor dictionary to generate a views key for
|
||
|
"""
|
||
|
view_id = action.get('view_id') or False
|
||
|
if isinstance(view_id, (list, tuple)):
|
||
|
view_id = view_id[0]
|
||
|
|
||
|
# providing at least one view mode is a requirement, not an option
|
||
|
view_modes = action['view_mode'].split(',')
|
||
|
|
||
|
if len(view_modes) > 1:
|
||
|
if view_id:
|
||
|
raise ValueError('Non-db action dictionaries should provide '
|
||
|
'either multiple view modes or a single view '
|
||
|
'mode and an optional view id.\n\n Got view '
|
||
|
'modes %r and view id %r for action %r' % (
|
||
|
view_modes, view_id, action))
|
||
|
action['views'] = [(False, mode) for mode in view_modes]
|
||
|
return
|
||
|
action['views'] = [(view_id, view_modes[0])]
|
||
|
|
||
|
|
||
|
def _get_login_redirect_url(uid, redirect=None):
|
||
|
""" Decide if user requires a specific post-login redirect, e.g. for 2FA, or if they are
|
||
|
fully logged and can proceed to the requested URL
|
||
|
"""
|
||
|
if request.session.uid: # fully logged
|
||
|
return redirect or ('/web' if is_user_internal(request.session.uid)
|
||
|
else '/web/login_successful')
|
||
|
|
||
|
# partial session (MFA)
|
||
|
url = request.env(user=uid)['res.users'].browse(uid)._mfa_url()
|
||
|
if not redirect:
|
||
|
return url
|
||
|
|
||
|
parsed = werkzeug.urls.url_parse(url)
|
||
|
qs = parsed.decode_query()
|
||
|
qs['redirect'] = redirect
|
||
|
return parsed.replace(query=werkzeug.urls.url_encode(qs)).to_url()
|
||
|
|
||
|
|
||
|
def is_user_internal(uid):
|
||
|
return request.env['res.users'].browse(uid)._is_internal()
|
||
|
|
||
|
|
||
|
def _local_web_translations(trans_file):
|
||
|
messages = []
|
||
|
try:
|
||
|
with file_open(trans_file, filter_ext=('.po')) as t_file:
|
||
|
po = babel.messages.pofile.read_po(t_file)
|
||
|
except Exception:
|
||
|
return
|
||
|
for x in po:
|
||
|
if x.id and x.string and (JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments
|
||
|
or WEB_TRANSLATION_COMMENT in x.auto_comments):
|
||
|
messages.append({'id': x.id, 'string': x.string})
|
||
|
return messages
|