summaryrefslogtreecommitdiff
path: root/addons/mail/controllers
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/controllers
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/controllers')
-rw-r--r--addons/mail/controllers/__init__.py4
-rw-r--r--addons/mail/controllers/bus.py65
-rw-r--r--addons/mail/controllers/home.py42
-rw-r--r--addons/mail/controllers/main.py296
4 files changed, 407 insertions, 0 deletions
diff --git a/addons/mail/controllers/__init__.py b/addons/mail/controllers/__init__.py
new file mode 100644
index 00000000..733774e9
--- /dev/null
+++ b/addons/mail/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*
+from . import main
+from . import bus
+from . import home
diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py
new file mode 100644
index 00000000..ccbe6bde
--- /dev/null
+++ b/addons/mail/controllers/bus.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import SUPERUSER_ID, tools
+from odoo.http import request, route
+from odoo.addons.bus.controllers.main import BusController
+
+
+class MailChatController(BusController):
+
+ def _default_request_uid(self):
+ """ For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none)
+ !!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access)
+ can access to the resource.
+ """
+ return request.session.uid and request.session.uid or SUPERUSER_ID
+
+ # --------------------------
+ # Extends BUS Controller Poll
+ # --------------------------
+ def _poll(self, dbname, channels, last, options):
+ if request.session.uid:
+ partner_id = request.env.user.partner_id.id
+
+ if partner_id:
+ channels = list(channels) # do not alter original list
+ for mail_channel in request.env['mail.channel'].search([('channel_partner_ids', 'in', [partner_id])]):
+ channels.append((request.db, 'mail.channel', mail_channel.id))
+ # personal and needaction channel
+ channels.append((request.db, 'res.partner', partner_id))
+ channels.append((request.db, 'ir.needaction', partner_id))
+ return super(MailChatController, self)._poll(dbname, channels, last, options)
+
+ # --------------------------
+ # Anonymous routes (Common Methods)
+ # --------------------------
+ @route('/mail/chat_post', type="json", auth="public", cors="*")
+ def mail_chat_post(self, uuid, message_content, **kwargs):
+ mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
+ if not mail_channel:
+ return False
+
+ # find the author from the user session
+ if request.session.uid:
+ author = request.env['res.users'].sudo().browse(request.session.uid).partner_id
+ author_id = author.id
+ email_from = author.email_formatted
+ else: # If Public User, use catchall email from company
+ author_id = False
+ email_from = mail_channel.anonymous_name or mail_channel.create_uid.company_id.catchall_formatted
+ # post a message without adding followers to the channel. email_from=False avoid to get author from email data
+ body = tools.plaintext2html(message_content)
+ message = mail_channel.with_context(mail_create_nosubscribe=True).message_post(author_id=author_id,
+ email_from=email_from, body=body,
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment')
+ return message and message.id or False
+
+ @route(['/mail/chat_history'], type="json", auth="public", cors="*")
+ def mail_chat_history(self, uuid, last_id=False, limit=20):
+ channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
+ if not channel:
+ return []
+ else:
+ return channel.channel_fetch_message(last_id, limit)
diff --git a/addons/mail/controllers/home.py b/addons/mail/controllers/home.py
new file mode 100644
index 00000000..e8c7a60d
--- /dev/null
+++ b/addons/mail/controllers/home.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+import ipaddress
+
+from odoo import _, SUPERUSER_ID
+from odoo.http import request
+from odoo.addons.web.controllers import main as web
+
+def _admin_password_warn(uid):
+ """ Admin still has `admin` password, flash a message via chatter.
+
+ Uses a private mail.channel from the system (/ odoobot) to the user, as
+ using a more generic mail.thread could send an email which is undesirable
+
+ Uses mail.channel directly because using mail.thread might send an email instead.
+ """
+ if request.params['password'] != 'admin':
+ return
+ if ipaddress.ip_address(request.httprequest.remote_addr).is_private:
+ return
+ env = request.env(user=SUPERUSER_ID, su=True)
+ admin = env.ref('base.partner_admin')
+ if uid not in admin.user_ids.ids:
+ return
+ has_demo = bool(env['ir.module.module'].search_count([('demo', '=', True)]))
+ if has_demo:
+ return
+
+ user = request.env(user=uid)['res.users']
+ MailChannel = env(context=user.context_get())['mail.channel']
+ MailChannel.browse(MailChannel.channel_get([admin.id])['id'])\
+ .message_post(
+ body=_("Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!"),
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment'
+ )
+
+class Home(web.Home):
+ def _login_redirect(self, uid, redirect=None):
+ if request.params.get('login_success'):
+ _admin_password_warn(uid)
+
+ return super()._login_redirect(uid, redirect)
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
new file mode 100644
index 00000000..4ecd87d3
--- /dev/null
+++ b/addons/mail/controllers/main.py
@@ -0,0 +1,296 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import logging
+import psycopg2
+import werkzeug.utils
+import werkzeug.wrappers
+
+from werkzeug.urls import url_encode
+
+from odoo import api, http, registry, SUPERUSER_ID, _
+from odoo.exceptions import AccessError
+from odoo.http import request
+from odoo.tools import consteq
+
+_logger = logging.getLogger(__name__)
+
+
+class MailController(http.Controller):
+ _cp_path = '/mail'
+
+ @classmethod
+ def _redirect_to_messaging(cls):
+ url = '/web#%s' % url_encode({'action': 'mail.action_discuss'})
+ return werkzeug.utils.redirect(url)
+
+ @classmethod
+ def _check_token(cls, token):
+ base_link = request.httprequest.path
+ params = dict(request.params)
+ params.pop('token', '')
+ valid_token = request.env['mail.thread']._notify_encode_link(base_link, params)
+ return consteq(valid_token, str(token))
+
+ @classmethod
+ def _check_token_and_record_or_redirect(cls, model, res_id, token):
+ comparison = cls._check_token(token)
+ if not comparison:
+ _logger.warning('Invalid token in route %s', request.httprequest.url)
+ return comparison, None, cls._redirect_to_messaging()
+ try:
+ record = request.env[model].browse(res_id).exists()
+ except Exception:
+ record = None
+ redirect = cls._redirect_to_messaging()
+ else:
+ redirect = cls._redirect_to_record(model, res_id)
+ return comparison, record, redirect
+
+ @classmethod
+ def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
+ # access_token and kwargs are used in the portal controller override for the Send by email or Share Link
+ # to give access to the record to a recipient that has normally no access.
+ uid = request.session.uid
+ user = request.env['res.users'].sudo().browse(uid)
+ cids = False
+
+ # no model / res_id, meaning no possible record -> redirect to login
+ if not model or not res_id or model not in request.env:
+ return cls._redirect_to_messaging()
+
+ # find the access action using sudo to have the details about the access link
+ RecordModel = request.env[model]
+ record_sudo = RecordModel.sudo().browse(res_id).exists()
+ if not record_sudo:
+ # record does not seem to exist -> redirect to login
+ return cls._redirect_to_messaging()
+
+ # the record has a window redirection: check access rights
+ if uid is not None:
+ if not RecordModel.with_user(uid).check_access_rights('read', raise_exception=False):
+ return cls._redirect_to_messaging()
+ try:
+ # We need here to extend the "allowed_company_ids" to allow a redirection
+ # to any record that the user can access, regardless of currently visible
+ # records based on the "currently allowed companies".
+ cids = request.httprequest.cookies.get('cids', str(user.company_id.id))
+ cids = [int(cid) for cid in cids.split(',')]
+ try:
+ record_sudo.with_user(uid).with_context(allowed_company_ids=cids).check_access_rule('read')
+ except AccessError:
+ # In case the allowed_company_ids from the cookies (i.e. the last user configuration
+ # on his browser) is not sufficient to avoid an ir.rule access error, try to following
+ # heuristic:
+ # - Guess the supposed necessary company to access the record via the method
+ # _get_mail_redirect_suggested_company
+ # - If no company, then redirect to the messaging
+ # - Merge the suggested company with the companies on the cookie
+ # - Make a new access test if it succeeds, redirect to the record. Otherwise,
+ # redirect to the messaging.
+ suggested_company = record_sudo._get_mail_redirect_suggested_company()
+ if not suggested_company:
+ raise AccessError('')
+ cids = cids + [suggested_company.id]
+ record_sudo.with_user(uid).with_context(allowed_company_ids=cids).check_access_rule('read')
+ except AccessError:
+ return cls._redirect_to_messaging()
+ else:
+ record_action = record_sudo.get_access_action(access_uid=uid)
+ else:
+ record_action = record_sudo.get_access_action()
+ if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
+ return cls._redirect_to_messaging()
+
+ record_action.pop('target_type', None)
+ # the record has an URL redirection: use it directly
+ if record_action['type'] == 'ir.actions.act_url':
+ return werkzeug.utils.redirect(record_action['url'])
+ # other choice: act_window (no support of anything else currently)
+ elif not record_action['type'] == 'ir.actions.act_window':
+ return cls._redirect_to_messaging()
+
+ url_params = {
+ 'model': model,
+ 'id': res_id,
+ 'active_id': res_id,
+ 'action': record_action.get('id'),
+ }
+ view_id = record_sudo.get_formview_id()
+ if view_id:
+ url_params['view_id'] = view_id
+
+ if cids:
+ url_params['cids'] = ','.join([str(cid) for cid in cids])
+ url = '/web?#%s' % url_encode(url_params)
+ return werkzeug.utils.redirect(url)
+
+ @http.route('/mail/read_followers', type='json', auth='user')
+ def read_followers(self, res_model, res_id):
+ request.env['mail.followers'].check_access_rights("read")
+ request.env[res_model].check_access_rights("read")
+ request.env[res_model].browse(res_id).check_access_rule("read")
+ follower_recs = request.env['mail.followers'].search([('res_model', '=', res_model), ('res_id', '=', res_id)])
+
+ followers = []
+ follower_id = None
+ for follower in follower_recs:
+ if follower.partner_id == request.env.user.partner_id:
+ follower_id = follower.id
+ followers.append({
+ 'id': follower.id,
+ 'partner_id': follower.partner_id.id,
+ 'channel_id': follower.channel_id.id,
+ 'name': follower.name,
+ 'display_name': follower.display_name,
+ 'email': follower.email,
+ 'is_active': follower.is_active,
+ # When editing the followers, the "pencil" icon that leads to the edition of subtypes
+ # should be always be displayed and not only when "debug" mode is activated.
+ 'is_editable': True
+ })
+ return {
+ 'followers': followers,
+ 'subtypes': self.read_subscription_data(follower_id) if follower_id else None
+ }
+
+ @http.route('/mail/read_subscription_data', type='json', auth='user')
+ def read_subscription_data(self, follower_id):
+ """ Computes:
+ - message_subtype_data: data about document subtypes: which are
+ available, which are followed if any """
+ request.env['mail.followers'].check_access_rights("read")
+ follower = request.env['mail.followers'].sudo().browse(follower_id)
+ follower.ensure_one()
+ request.env[follower.res_model].check_access_rights("read")
+ request.env[follower.res_model].browse(follower.res_id).check_access_rule("read")
+
+ # find current model subtypes, add them to a dictionary
+ subtypes = request.env['mail.message.subtype'].search([
+ '&', ('hidden', '=', False),
+ '|', ('res_model', '=', follower.res_model), ('res_model', '=', False)])
+ followed_subtypes_ids = set(follower.subtype_ids.ids)
+ subtypes_list = [{
+ 'name': subtype.name,
+ 'res_model': subtype.res_model,
+ 'sequence': subtype.sequence,
+ 'default': subtype.default,
+ 'internal': subtype.internal,
+ 'followed': subtype.id in followed_subtypes_ids,
+ 'parent_model': subtype.parent_id.res_model,
+ 'id': subtype.id
+ } for subtype in subtypes]
+ return sorted(subtypes_list,
+ key=lambda it: (it['parent_model'] or '', it['res_model'] or '', it['internal'], it['sequence']))
+
+ @http.route('/mail/view', type='http', auth='public')
+ def mail_action_view(self, model=None, res_id=None, access_token=None, **kwargs):
+ """ Generic access point from notification emails. The heuristic to
+ choose where to redirect the user is the following :
+
+ - find a public URL
+ - if none found
+ - users with a read access are redirected to the document
+ - users without read access are redirected to the Messaging
+ - not logged users are redirected to the login page
+
+ models that have an access_token may apply variations on this.
+ """
+ # ==============================================================================================
+ # This block of code disappeared on saas-11.3 to be reintroduced by TBE.
+ # This is needed because after a migration from an older version to saas-11.3, the link
+ # received by mail with a message_id no longer work.
+ # So this block of code is needed to guarantee the backward compatibility of those links.
+ if kwargs.get('message_id'):
+ try:
+ message = request.env['mail.message'].sudo().browse(int(kwargs['message_id'])).exists()
+ except:
+ message = request.env['mail.message']
+ if message:
+ model, res_id = message.model, message.res_id
+ # ==============================================================================================
+
+ if res_id and isinstance(res_id, str):
+ res_id = int(res_id)
+ return self._redirect_to_record(model, res_id, access_token, **kwargs)
+
+ @http.route('/mail/assign', type='http', auth='user', methods=['GET'])
+ def mail_action_assign(self, model, res_id, token=None):
+ comparison, record, redirect = self._check_token_and_record_or_redirect(model, int(res_id), token)
+ if comparison and record:
+ try:
+ record.write({'user_id': request.uid})
+ except Exception:
+ return self._redirect_to_messaging()
+ return redirect
+
+ @http.route('/mail/<string:res_model>/<int:res_id>/avatar/<int:partner_id>', type='http', auth='public')
+ def avatar(self, res_model, res_id, partner_id):
+ headers = [('Content-Type', 'image/png')]
+ status = 200
+ content = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' # default image is one white pixel
+ if res_model in request.env:
+ try:
+ # if the current user has access to the document, get the partner avatar as sudo()
+ request.env[res_model].browse(res_id).check_access_rule('read')
+ if partner_id in request.env[res_model].browse(res_id).sudo().exists().message_ids.mapped('author_id').ids:
+ status, headers, _content = request.env['ir.http'].sudo().binary_content(
+ model='res.partner', id=partner_id, field='image_128', default_mimetype='image/png')
+ # binary content return an empty string and not a placeholder if obj[field] is False
+ if _content != '':
+ content = _content
+ if status == 304:
+ return werkzeug.wrappers.Response(status=304)
+ except AccessError:
+ pass
+ image_base64 = base64.b64decode(content)
+ headers.append(('Content-Length', len(image_base64)))
+ response = request.make_response(image_base64, headers)
+ response.status = str(status)
+ return response
+
+ @http.route('/mail/needaction', type='json', auth='user')
+ def needaction(self):
+ return request.env['res.partner'].get_needaction_count()
+
+ @http.route('/mail/init_messaging', type='json', auth='user')
+ def mail_init_messaging(self):
+ values = {
+ 'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
+ 'starred_counter': request.env['res.partner'].get_starred_count(),
+ 'channel_slots': request.env['mail.channel'].channel_fetch_slot(),
+ 'mail_failures': request.env['mail.message'].message_fetch_failed(),
+ 'commands': request.env['mail.channel'].get_mention_commands(),
+ 'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
+ 'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
+ 'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.menu_root_discuss'),
+ 'moderation_counter': request.env.user.moderation_counter,
+ 'moderation_channel_ids': request.env.user.moderation_channel_ids.ids,
+ 'partner_root': request.env.ref('base.partner_root').sudo().mail_partner_format(),
+ 'public_partner': request.env.ref('base.public_partner').sudo().mail_partner_format(),
+ 'public_partners': [partner.mail_partner_format() for partner in request.env.ref('base.group_public').sudo().with_context(active_test=False).users.partner_id],
+ 'current_partner': request.env.user.partner_id.mail_partner_format(),
+ 'current_user_id': request.env.user.id,
+ }
+ return values
+
+ @http.route('/mail/get_partner_info', type='json', auth='user')
+ def message_partner_info_from_emails(self, model, res_ids, emails, link_mail=False):
+ records = request.env[model].browse(res_ids)
+ try:
+ records.check_access_rule('read')
+ records.check_access_rights('read')
+ except:
+ return []
+ return records._message_partner_info_from_emails(emails, link_mail=link_mail)
+
+ @http.route('/mail/get_suggested_recipients', type='json', auth='user')
+ def message_get_suggested_recipients(self, model, res_ids):
+ records = request.env[model].browse(res_ids)
+ try:
+ records.check_access_rule('read')
+ records.check_access_rights('read')
+ except:
+ return {}
+ return records._message_get_suggested_recipients()