def _get_record_and_check(self, xmlid=None, model=None, id=None, field='datas', access_token=None): # get object and content record = None if xmlid: record = self._xmlid_to_obj(self.env, xmlid) elif id and model in self.env: record = self.env[model].browse(int(id)) # obj exists if not record or not record.exists() or field not in record: return None, 404 if model == 'ir.attachment': record_sudo = record.sudo() if access_token and not consteq(record_sudo.access_token or '', access_token): return None, 403 elif (access_token and consteq(record_sudo.access_token or '', access_token)): record = record_sudo elif record_sudo.public: record = record_sudo elif self.env.user.has_group('base.group_portal'): # Check the read access on the record linked to the attachment # eg: Allow to download an attachment on a task from /my/task/task_id record.check('read') record = record_sudo # check read access try: record['__last_update'] except AccessError: return None, 403 return record, 200
def _check_special_access(res_model, res_id, token='', _hash='', pid=False): record = request.env[res_model].browse(res_id).sudo() if token: # Token Case: token is the global one of the document token_field = request.env[res_model]._mail_post_token_field return (token and record and consteq(record[token_field], token)) elif _hash and pid: # Signed Token Case: hash implies token is signed by partner pid return consteq(_hash, record._sign_token(pid)) else: raise Forbidden()
def _verify_stripe_signature(self): """ :return: true if and only if signature matches hash of payload calculated with secret :raises ValidationError: if signature doesn't match """ if not self.stripe_webhook_secret: raise ValidationError( 'webhook event received but webhook secret is not configured') signature = request.httprequest.headers.get('Stripe-Signature') body = request.httprequest.data sign_data = { k: v for (k, v) in [s.split('=') for s in signature.split(',')] } event_timestamp = int(sign_data['t']) if datetime.utcnow().timestamp( ) - event_timestamp > STRIPE_SIGNATURE_AGE_TOLERANCE: _logger.error('stripe event is too old, event is discarded') raise ValidationError('event timestamp older than tolerance') signed_payload = "%s.%s" % (event_timestamp, body.decode('utf-8')) actual_signature = sign_data['v1'] expected_signature = hmac.new( self.stripe_webhook_secret.encode('utf-8'), signed_payload.encode('utf-8'), sha256).hexdigest() if not consteq(expected_signature, actual_signature): _logger.error( 'incorrect webhook signature from Stripe, check if the webhook signature ' 'in Odoo matches to one in the Stripe dashboard') raise ValidationError('incorrect webhook signature') return True
def _retrieve_api_key_id(self, key): if not self.env.user.has_group("base.group_system"): raise AccessError(_("User is not allowed")) for api_key in self.search([]): if consteq(key, api_key.key): return api_key.id raise ValidationError(_("The key %s is not allowed") % key)
def download_one(self, id=None, access_token=None, share_id=None, **kwargs): """ used to download a single file from the portal multi-file page. :param id: id of the file :param access_token: token of the share link :param share_id: id of the share link :return: a portal page to preview and download a single file. """ env = request.env share = env['documents.share'].sudo().browse(share_id) if consteq(access_token, share.access_token): try: if share.action != 'upload' and share.state != 'expired': return self._get_file_response( id, share_id=share_id, share_token=share.access_token, field='datas') except Exception: logger.exception("Failed to download attachment %s" % id) return request.not_found()
def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs): """ If the current user doesn't have access to the document, but provided a valid access token, redirect him to the front-end view. If the partner_id and hash parameters are given, add those parameters to the redirect url to authentify the recipient in the chatter, if any. :param model: the model name of the record that will be visualized :param res_id: the id of the record :param access_token: token that gives access to the record bypassing the rights and rules restriction of the user. :param kwargs: Typically, it can receive a partner_id and a hash (sign_token). If so, those two parameters are used to authentify the recipient in the chatter, if any. :return: """ if issubclass(type(request.env[model]), request.env.registry['portal.mixin']): uid = request.session.uid or request.env.ref('base.public_user').id record_sudo = request.env[model].sudo().browse(res_id).exists() try: record_sudo.sudo(uid).check_access_rights('read') record_sudo.sudo(uid).check_access_rule('read') except AccessError: if record_sudo.access_token and access_token and consteq(record_sudo.access_token, access_token): record_action = record_sudo.with_context(force_website=True).get_access_action() if record_action['type'] == 'ir.actions.act_url': pid = kwargs.get('pid') hash = kwargs.get('hash') url = record_action['url'] if pid and hash: url = urls.url_parse(url) url_params = url.decode_query() url_params.update([("pid", pid), ("hash", hash)]) url = url.replace(query=urls.url_encode(url_params)).to_url() return werkzeug.utils.redirect(url) return super(MailController, cls)._redirect_to_record(model, res_id, access_token=access_token)
def share_download_all(self, access_token=None, share_id=None, **kwargs): """ :param share_id: id of the share, the name of the share will be the name of the zip file share. :param access_token: share access token :returns the http response for a zip file if the token and the ID are valid. """ env = request.env try: share = env['documents.share'].sudo().browse(share_id) if share.state == 'expired': return request.not_found() if consteq(access_token, share.access_token): if share.action != 'upload': attachments = False if share.type == 'domain': domain = [] if share.domain: domain = literal_eval(share.domain) domain = expression.AND( [domain, [['folder_id', '=', share.folder_id.id]]]) attachments = http.request.env['ir.attachment'].sudo( ).search(domain) elif share.type == 'ids': attachments = share.attachment_ids return self._make_zip( (share.name or 'unnamed-link') + '.zip', attachments) except Exception: logger.exception("Failed to zip share link id: %s" % share_id) return request.not_found()
def check_token(self, access_token, partner_id, amount, currency_id): secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') token_str = '%s%s%s' % (partner_id, amount, currency_id) correct_token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest() if consteq(ustr(access_token), correct_token): return True return False
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs): channel_sudo = request.env['mail.channel'].browse(int(channel_id)).sudo().exists() if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, invitation_token): raise NotFound() if channel_sudo.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=int(channel_id)): return request.redirect(f'/discuss/channel/{channel_sudo.id}') if channel_sudo.channel_type == 'chat': raise NotFound() response = request.redirect(f'/discuss/channel/{channel_sudo.id}/welcome') if not channel_sudo.env.user._is_public(): channel_sudo.add_members([channel_sudo.env.user.partner_id.id]) return response guest = channel_sudo.env['mail.guest']._get_guest_from_request(request) if not guest: guest = channel_sudo.env['mail.guest'].create({ 'country_id': channel_sudo.env['res.country'].search([('code', '=', request.session.get('geoip', {}).get('country_code'))], limit=1).id, 'lang': get_lang(channel_sudo.env).code, 'name': _("Guest"), 'timezone': channel_sudo.env['mail.guest']._get_timezone_from_request(request), }) # Discuss Guest ID: every route in this file will make use of it to authenticate # the guest through `_get_as_sudo_from_request` or `_get_as_sudo_from_request_or_raise`. expiration_date = datetime.now() + timedelta(days=365) response.set_cookie(guest._cookie_name, f"{guest.id}{guest._cookie_separator}{guest.access_token}", httponly=True, expires=expiration_date) channel_sudo.add_members(guest_ids=[guest.id]) return response
def _document_check_access(self, model_name, document_id, access_token=None): """Check if current user is allowed to access the specified record. :param str model_name: model of the requested record :param int document_id: id of the requested record :param str access_token: record token to check if user isn't allowed to read requested record :return: expected record, SUDOED, with SUPERUSER context :raise MissingError: record not found in database, might have been deleted :raise AccessError: current user isn't allowed to read requested document (and no valid token was given) """ document = request.env[model_name].browse([document_id]) document_sudo = document.with_user(SUPERUSER_ID).exists() if not document_sudo: raise MissingError(_("This document does not exist.")) try: document.check_access_rights('read') document.check_access_rule('read') except AccessError: if not access_token or not document_sudo.access_token or not consteq( document_sudo.access_token, access_token): raise return document_sudo
def _get_record_and_check(self, xmlid=None, model=None, id=None, field='datas', access_token=None): # get object and content record = None if xmlid: record = self._xmlid_to_obj(self.env, xmlid) elif id and model in self.env: record = self.env[model].browse(int(id)) # obj exists if not record or not record.exists() or field not in record: return None, 404 # access token grant access if model == 'ir.attachment' and access_token: record = record.sudo() if not consteq(record.access_token or '', access_token): return None, 403 # check read access try: last_update = record['__last_update'] except AccessError: return None, 403 return record, 200
def article_invite(self, member_id, invitation_hash): """ This route will check if the given parameter allows the client to access the article via the invite token. Then, if the partner has not registered yet, we will redirect the client to the signup page to finally redirect them to the article. If the partner already has registrered, we redirect them directly to the article. """ member = request.env['knowledge.article.member'].sudo().browse(member_id).exists() correct_token = member._get_invitation_hash() if member else False if not correct_token or not tools.consteq(correct_token, invitation_hash): raise werkzeug.exceptions.NotFound() partner = member.partner_id article = member.article_id if not partner.user_ids: # Force the signup even if not enabled (as we explicitly invited the member). # They should still be able to create a user. signup_allowed = request.env['res.users']._get_signup_invitation_scope() == 'b2c' if not signup_allowed: partner.signup_prepare() partner.signup_get_auth_param() signup_url = partner._get_signup_url_for_action(url='/knowledge/article/%s' % article.id)[partner.id] return request.redirect(signup_url) return request.redirect('/web/login?redirect=/knowledge/article/%s' % article.id)
def _check_token(self, access_token): if not access_token: return False try: return consteq(access_token, self.access_token) except: return False
def confirm(self, **kw): tx_id = int(kw.get('tx_id', 0)) access_token = kw.get('access_token') if tx_id: if access_token: tx = request.env['payment.transaction'].sudo().browse(tx_id) secret = request.env['ir.config_parameter'].sudo().get_param('database.secret') valid_token_str = '%s%s%s' % (tx.id, tx.reference, float_repr(tx.amount, precision_digits=tx.currency_id.decimal_places)) valid_token = hmac.new(secret.encode('utf-8'), valid_token_str.encode('utf-8'), hashlib.sha256).hexdigest() if not consteq(ustr(valid_token), access_token): raise werkzeug.exceptions.NotFound else: tx = request.env['payment.transaction'].browse(tx_id) if tx.state in ['done', 'authorized']: status = 'success' message = tx.acquirer_id.done_msg elif tx.state == 'pending': status = 'warning' message = tx.acquirer_id.pending_msg else: status = 'danger' message = tx.state_message or _('An error occured during the processing of this payment') PaymentProcessing.remove_payment_transaction(tx) return request.render('payment.confirm', {'tx': tx, 'status': status, 'message': message}) else: return request.redirect('/my/home')
def mail_attachment_delete(self, attachment_id, access_token=None, **kwargs): attachment_sudo = request.env['ir.attachment'].browse( int(attachment_id)).sudo().exists() if not attachment_sudo: raise NotFound() if not request.env.user.share: # Check through standard access rights/rules for internal users. return attachment_sudo.sudo(False).unlink() # For non-internal users 2 cases are supported: # - Either the attachment is linked to a message: verify the request is made by the author of the message (portal user or guest). # - Either a valid access token is given: also verify the message is pending (because unfortunately in portal a token is also provided to guest for viewing others' attachments). message_sudo = request.env['mail.message'].sudo().search( [('attachment_ids', 'in', attachment_sudo.ids)], limit=1) if message_sudo: if request.session.uid and ( not message_sudo.author_id or message_sudo.author_id != request.env.user.partner_id): raise NotFound() if not request.session.uid and ( not message_sudo.author_guest_id or message_sudo.author_guest_id != request. env['mail.guest']._get_guest_from_request(request)): raise NotFound() else: if not access_token or not attachment_sudo.access_token or not consteq( access_token, attachment_sudo.access_token): raise NotFound() if attachment_sudo.res_model != 'mail.compose.message' or attachment_sudo.res_id != 0: raise NotFound() return attachment_sudo.unlink()
def execute_callback(self): res = None for transaction in self: # limited sudo env, only for checking callback presence, not for running it! # manual transactions have no callback, and can pass without being run by admin user tx_sudo = transaction.sudo() if not (tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method): continue valid_token = transaction._generate_callback_hash() if not consteq(ustr(valid_token), transaction.callback_hash): _logger.warning( "Invalid callback signature for transaction %d" % (transaction.id)) continue record = self.env[transaction.callback_model_id.model].browse( transaction.callback_res_id).exists() if record: res = getattr(record, transaction.callback_method)(transaction) else: _logger.warning( "Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id)) return res
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))
def _get_api_key_name(cls, auth_api_key): for section in serv_config.sections(): if section.startswith("api_key_") and serv_config.has_option( section, "key"): if tools.consteq(auth_api_key, serv_config.get(section, "key")): return section return None
def share_portal(self, share_id=None, token=None): """ Leads to a public portal displaying downloadable files for anyone with the token. :param share_id: id of the share link :param token: share access token """ try: share = http.request.env['documents.share'].sudo().search([ ('id', '=', share_id) ]) if share.state == 'expired': expired_options = { 'expiration_date': share.date_deadline, 'author': share.create_uid.name, } return request.render('documents.not_available', expired_options) if not consteq(token, share.access_token): return request.not_found() if share.type == 'domain': domain = [] if share.domain: domain = literal_eval(share.domain) domain += [['folder_id', '=', share.folder_id.id]] attachments = http.request.env['ir.attachment'].sudo().search( domain) elif share.type == 'ids': attachments = share.attachment_ids else: return request.not_found() options = { 'base_url': http.request.env["ir.config_parameter"].sudo().get_param( "web.base.url"), 'token': str(token), 'upload': share.action == 'downloadupload', 'share_id': str(share.id), 'author': share.create_uid.name, } if len(attachments) == 1 and share.type == 'ids': options.update(attachment=attachments[0]) return request.render('documents.share_single', options) else: options.update(all_button='binary' in [ attachment.type for attachment in attachments ], attachment_ids=attachments) return request.render('documents.share_page', options) except Exception: logger.exception("Failed to generate the multi file share portal") return request.not_found()
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs): channel_sudo = request.env['mail.channel'].browse( channel_id).sudo().exists() if not channel_sudo or not channel_sudo.uuid or not consteq( channel_sudo.uuid, invitation_token): raise NotFound() return self._response_discuss_channel_invitation( channel_sudo=channel_sudo)
def _get_record_and_check(self, xmlid=None, model=None, id=None, field='datas', access_token=None): # get object and content record = None if xmlid: record = self._xmlid_to_obj(self.env, xmlid) elif id and model in self.env: record = self.env[model].browse(int(id)) # obj exists if not record or not record.exists() or field not in record: return None, 404 if model == 'ir.attachment': record_sudo = record.sudo() if access_token and not consteq(record_sudo.access_token or '', access_token): return None, 403 elif (access_token and consteq(record_sudo.access_token or '', access_token)): record = record_sudo elif record_sudo.public: record = record_sudo elif self.env.user.has_group('base.group_portal'): # Check the read access on the record linked to the attachment # eg: Allow to download an attachment on a task from /my/task/task_id record.check('read') record = record_sudo # We have prefetched some fields of record, among which the field # 'write_date' used by '__last_update' below. In order to check # access on record, we have to invalidate its cache first. record._cache.clear() # check read access try: record['__last_update'] except AccessError: return None, 403 return record, 200
def _contact_check_access(self, contact_id, access_token=None): contact = request.env['hr.employee'].browse([contact_id]) contact_sudo = contact.sudo() try: contact.check_access_rights('read') contact.check_access_rule('read') except AccessError: if not access_token or not consteq(contact_sudo.access_token, access_token): raise return contact_sudo
def _invoice_check_access(self, invoice_id, access_token=None): invoice = request.env['account.invoice'].browse([invoice_id]) invoice_sudo = invoice.sudo() try: invoice.check_access_rights('read') invoice.check_access_rule('read') except AccessError: if not access_token or not consteq(invoice_sudo.access_token, access_token): raise return invoice_sudo
def _order_check_access(self, order_id, access_token=None): order = request.env['sale.order'].browse([order_id]) order_sudo = order.sudo() try: order.check_access_rights('read') order.check_access_rule('read') except AccessError: if not access_token or not consteq(order_sudo.access_token, access_token): raise return order_sudo
def _stock_picking_check_access(self, picking_id, access_token=None): picking = request.env['stock.picking'].browse([picking_id]) picking_sudo = picking.sudo() try: picking.check_access_rights('read') picking.check_access_rule('read') except exceptions.AccessError: if not access_token or not consteq(picking_sudo.sale_id.access_token, access_token): raise return picking_sudo
def _picking_check_access(self, rma_id, picking_id, access_token=None): rma = request.env['rma'].browse([rma_id]) picking = request.env['stock.picking'].browse([picking_id]) picking_sudo = picking.sudo() try: picking.check_access_rights('read') picking.check_access_rule('read') except exceptions.AccessError: if not access_token or not consteq(rma.access_token, access_token): raise return picking_sudo
def track_mail_open(self, mail_id, token, **post): """ Email tracking. """ if not consteq(token, tools.hmac(request.env(su=True), 'mass_mailing-mail_mail-open', mail_id)): raise BadRequest() request.env['mailing.trace'].sudo().set_opened(mail_mail_ids=[mail_id]) response = werkzeug.wrappers.Response() response.mimetype = 'image/gif' response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==') return response
def _ad_blocks_check_access(self, ad_block_id, access_token=None): ad_blocks = request.env['sale.order.line'].browse([ad_block_id]) ad_blocks_sudo = ad_blocks.sudo() try: ad_blocks.check_access_rights('read') ad_blocks.check_access_rule('read') except AccessError: if not access_token or not consteq(ad_blocks_sudo.access_token, access_token): raise return ad_blocks
def _appointments_check_access(self, appoint_id, access_token=None): appointments = request.env['appointment'].browse([appoint_id]) appointments_sudo = appointments.sudo() try: appointments.check_access_rights('read') appointments.check_access_rule('read') except AccessError: if not access_token or not consteq(appointments_sudo.access_token, access_token): raise return appointments_sudo
def _br_check_access(self, br_id, access_token=None): br = request.env['business.requirement'].browse([br_id]) br_sudo = br.sudo() try: br.check_access_rights('read') br.check_access_rule('read') except AccessError: if not access_token or not consteq(br_sudo.access_token, access_token): raise return br_sudo
def _document_check_access(self, model_name, document_id, access_token=None): document = request.env[model_name].browse([document_id]) document_sudo = document.with_user(SUPERUSER_ID).exists() if not document_sudo: raise MissingError(_("This document does not exist.")) try: document.check_access_rights('read') document.check_access_rule('read') except AccessError: if not access_token or not document_sudo.access_token or not consteq(document_sudo.access_token, access_token): raise return document_sudo
def _document_check_access(self, model_name, document_id, access_token=None): document = request.env[model_name].browse([document_id]) document_sudo = document.sudo().exists() if not document_sudo: raise MissingError("This document does not exist.") try: document.check_access_rights('read') document.check_access_rule('read') except AccessError: if not access_token or not consteq(document_sudo.access_token, access_token): raise return document_sudo
def _message_post_helper(res_model='', res_id=None, message='', token='', nosubscribe=True, **kw): """ Generic chatter function, allowing to write on *any* object that inherits mail.thread. If a token is specified, all logged in users will be able to write a message regardless of access rights; if the user is the public user, the message will be posted under the name of the partner_id of the object (or the public user if there is no partner_id on the object). :param string res_model: model name of the object :param int res_id: id of the object :param string message: content of the message optional keywords arguments: :param string token: access token if the object's model uses some kind of public access using tokens (usually a uuid4) to bypass access rules :param bool nosubscribe: set False if you want the partner to be set as follower of the object when posting (default to True) The rest of the kwargs are passed on to message_post() """ record = request.env[res_model].browse(res_id) author_id = request.env.user.partner_id.id if request.env.user.partner_id else False if token: access_as_sudo = _has_token_access(res_model, res_id, token=token) if access_as_sudo: record = record.sudo() if request.env.user._is_public(): if kw.get('pid') and consteq( kw.get('hash'), record._sign_token(int( kw.get('pid')))): author_id = kw.get('pid') else: # TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line # TODO : Author must be Public User (to rename to 'Anonymous') author_id = record.partner_id.id if hasattr( record, 'partner_id') and record.partner_id.id else author_id else: if not author_id: raise NotFound() else: raise Forbidden() kw.pop('csrf_token', None) kw.pop('attachment_ids', None) return record.with_context( mail_create_nosubscribe=nosubscribe).message_post( body=message, message_type=kw.pop('message_type', "comment"), subtype=kw.pop('subtype', "mt_comment"), author_id=author_id, **kw)
def check_access_token(access_token, *values): """ Check the validity of the access token for the provided values. The values must be provided in the exact same order as they were to `generate_access_token`. All values must be convertible to a string. :param str access_token: The access token used to verify the provided values :param list values: The values to verify against the token :return: True if the check is successful :rtype: bool """ authentic_token = generate_access_token(*values) return access_token and consteq(ustr(access_token), authentic_token)
def _get_guest_from_request(self, request): guest_id = request.httprequest.cookies.get('mail.guest_id') guest_access_token = request.httprequest.cookies.get('mail.guest_access_token') if not guest_id or not guest_access_token: return self.env['mail.guest'] guest = self.env['mail.guest'].browse(int(guest_id)).sudo().exists() if not guest or not guest.access_token or not consteq(guest.access_token, guest_access_token): return self.env['mail.guest'] if not guest.timezone: timezone = self._get_timezone_from_request(request) if timezone: guest._update_timezone(timezone) return guest.sudo(False).with_context(guest=guest)
def execute_callback(self): res = None for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.callback_method): valid_token = transaction._generate_callback_hash() if not consteq(ustr(valid_token), transaction.callback_hash): _logger.warning("Invalid callback signature for transaction %d" % (transaction.id)) continue record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists() if record: res = getattr(record, transaction.callback_method)(transaction) else: _logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id)) return res
def portal_my_invoice_detail(self, invoice_id, access_token=None, **kw): invoice = request.env['account.invoice'].browse(invoice_id) try: invoice.check_access_rights('read') invoice.check_access_rule('read') except AccessError: if not access_token or not consteq(invoice.sudo().access_token, access_token): return request.redirect('/my') values = { 'page_name': 'invoice', 'invoice': invoice.sudo(), } return request.render("account.portal_invoice_page", values)
def mailing(self, mailing_id, email=None, res_id=None, token="", **post): mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) if mailing.exists(): res_id = res_id and int(res_id) right_token = mailing._unsubscribe_token(res_id, email) if not consteq(str(token), right_token): raise exceptions.AccessDenied() if mailing.mailing_model_real == 'mail.mass_mailing.contact': # Unsubscribe directly + Let the user choose his subscriptions mailing.update_opt_out(email, mailing.contact_list_ids.ids, True) contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email', '=', email)]) subscription_list_ids = contacts.mapped('subscription_list_ids') # In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2, # assume that the user is not opt_out on both # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in opt_out_list_ids = subscription_list_ids.filtered(lambda rel: rel.opt_out).mapped('list_id') opt_in_list_ids = subscription_list_ids.filtered(lambda rel: not rel.opt_out).mapped('list_id') opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids]) unique_list_ids = set([list.list_id.id for list in subscription_list_ids]) list_ids = request.env['mail.mass_mailing.list'].sudo().browse(unique_list_ids) unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public) return request.render('mass_mailing.page_unsubscribe', { 'contacts': contacts, 'list_ids': list_ids, 'opt_out_list_ids': opt_out_list_ids, 'unsubscribed_list': unsubscribed_list, 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'), }) else: blacklist_rec = request.env['mail.blacklist'].sudo()._add(email) blacklist_rec._message_log(_("""The %s asked to not be contacted anymore using an unsubscribe link.""" % request.env['ir.model']._get(mailing.mailing_model_real).display_name)) return request.render('mass_mailing.page_unsubscribed', { 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param( 'mass_mailing.show_blacklist_buttons'), }) return request.redirect('/web')
def portal_my_invoice_report(self, invoice_id, access_token=None, **kw): invoice = request.env['account.invoice'].browse(invoice_id) try: invoice.check_access_rights('read') invoice.check_access_rule('read') except AccessError: if not access_token or not consteq(invoice.sudo().access_token, access_token): return request.redirect('/my') # print report as sudo, since it require access to taxes, payment term, ... and portal # does not have those access rights. pdf = request.env.ref('account.account_invoices').sudo().render_qweb_pdf([invoice_id])[0] pdfhttpheaders = [ ('Content-Type', 'application/pdf'), ('Content-Length', len(pdf)), ] return request.make_response(pdf, headers=pdfhttpheaders)
def mailing(self, mailing_id, email=None, res_id=None, token="", **post): mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) if mailing.exists(): res_ids = [] if mailing.mailing_model == 'mail.mass_mailing.contact': contacts = request.env['mail.mass_mailing.contact'].sudo().search([ ('email', '=', email), ('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids]) ]) res_ids = contacts.ids else: res_ids = [res_id] right_token = mailing._unsubscribe_token(res_id, email) if not consteq(token, right_token): raise exceptions.AccessDenied() mailing.update_opt_out(email, res_ids, True) return _('You have been unsubscribed successfully')
def _message_post_helper(res_model='', res_id=None, message='', token='', nosubscribe=True, **kw): """ Generic chatter function, allowing to write on *any* object that inherits mail.thread. If a token is specified, all logged in users will be able to write a message regardless of access rights; if the user is the public user, the message will be posted under the name of the partner_id of the object (or the public user if there is no partner_id on the object). :param string res_model: model name of the object :param int res_id: id of the object :param string message: content of the message optional keywords arguments: :param string token: access token if the object's model uses some kind of public access using tokens (usually a uuid4) to bypass access rules :param bool nosubscribe: set False if you want the partner to be set as follower of the object when posting (default to True) The rest of the kwargs are passed on to message_post() """ record = request.env[res_model].browse(res_id) author_id = request.env.user.partner_id.id if request.env.user.partner_id else False if token: access_as_sudo = _has_token_access(res_model, res_id, token=token) if access_as_sudo: record = record.sudo() if request.env.user._is_public(): if kw.get('pid') and consteq(kw.get('hash'), record._sign_token(int(kw.get('pid')))): author_id = kw.get('pid') else: # TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line # TODO : Author must be Public User (to rename to 'Anonymous') author_id = record.partner_id.id if hasattr(record, 'partner_id') and record.partner_id.id else author_id else: if not author_id: raise NotFound() else: raise Forbidden() kw.pop('csrf_token', None) kw.pop('attachment_ids', None) return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message, message_type=kw.pop('message_type', "comment"), subtype=kw.pop('subtype', "mt_comment"), author_id=author_id, **kw)
def execute_callback(self): res = None for transaction in self: # limited sudo env, only for checking callback presence, not for running it! # manual transactions have no callback, and can pass without being run by admin user tx_sudo = transaction.sudo() if not (tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method): continue valid_token = transaction._generate_callback_hash() if not consteq(ustr(valid_token), transaction.callback_hash): _logger.warning("Invalid callback signature for transaction %d" % (transaction.id)) continue record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists() if record: res = getattr(record, transaction.callback_method)(transaction) else: _logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id)) return res
def orders_followup(self, order=None, access_token=None, **kw): order = request.env['sale.order'].browse([order]) order_sudo = order.sudo() values = {} try: order.check_access_rights('read') order.check_access_rule('read') except AccessError: if not access_token or not consteq(order_sudo.access_token, access_token): return request.render("website.403") values.update({'no_breadcrumbs': True}) order_invoice_lines = {il.product_id.id: il.invoice_id for il in order_sudo.invoice_ids.mapped('invoice_line_ids')} history = request.session.get('my_orders_history', []) values.update({ 'order': order_sudo, 'order_invoice_lines': order_invoice_lines, }) values.update(get_records_pager(history, order_sudo)) return request.render("website_portal_sale.orders_followup", values)
def _has_token_access(res_model, res_id, token=''): record = request.env[res_model].browse(res_id).sudo() token_field = request.env[res_model]._mail_post_token_field return (token and record and consteq(record[token_field], token))
def _valid_unsubscribe_token(self, mailing_id, res_id, email, token): if not (mailing_id and res_id and email and token): return False mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) return consteq(mailing._unsubscribe_token(res_id, email), token)
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', access_token=None, related_id=None, access_mode=None, env=None): """ Get file, attachment or downloadable content If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the binary field (via ``default_get``), otherwise fetches the field for that precise record. :param str xmlid: xmlid of the record :param str model: name of the model to fetch the binary from :param int id: id of the record from which to fetch the binary :param str field: binary field :param bool unique: add a max-age for the cache control :param str filename: choose a filename :param str filename_field: if not create an filename with model-id-field :param bool download: apply headers to download the file :param str mimetype: mintype of the field (for headers) :param related_id: the id of another record used for custom_check :param access_mode: if truthy, will call custom_check to fetch the object that contains the binary. :param str default_mimetype: default mintype if no mintype found :param str access_token: optional token for unauthenticated access only available for ir.attachment :param Environment env: by default use request.env :returns: (status, headers, content) """ env = env or request.env # get object and content obj = None if xmlid: obj = cls._xmlid_to_obj(env, xmlid) elif id and model in env.registry: obj = env[model].browse(int(id)) # obj exists if not obj or not obj.exists() or field not in obj: return (404, [], None) # access token grant access if model == 'ir.attachment' and access_token: obj = obj.sudo() if access_mode: if not cls._check_access_mode(env, id, access_mode, model, access_token=access_token, related_id=related_id): return (403, [], None) elif not consteq(obj.access_token or u'', access_token): return (403, [], None) # check read access try: last_update = obj['__last_update'] except AccessError: return (403, [], None) status, headers, content = None, [], None # attachment by url check module_resource_path = None if model == 'ir.attachment' and obj.type == 'url' and obj.url: url_match = re.match("^/(\w+)/(.+)$", obj.url) if url_match: module = url_match.group(1) module_path = get_module_path(module) module_resource_path = get_resource_path(module, url_match.group(2)) if module_path and module_resource_path: module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' module_resource_path = os.path.normpath(module_resource_path) if module_resource_path.startswith(module_path): with open(module_resource_path, 'rb') as f: content = base64.b64encode(f.read()) last_update = pycompat.text_type(os.path.getmtime(module_resource_path)) if not module_resource_path: module_resource_path = obj.url if not content: status = 301 content = module_resource_path else: content = obj[field] or '' # filename if not filename: if filename_field in obj: filename = obj[filename_field] elif module_resource_path: filename = os.path.basename(module_resource_path) else: filename = "%s-%s-%s" % (obj._name, obj.id, field) # mimetype mimetype = 'mimetype' in obj and obj.mimetype or False if not mimetype: if filename: mimetype = mimetypes.guess_type(filename)[0] if not mimetype and getattr(env[model]._fields[field], 'attachment', False): # for binary fields, fetch the ir_attachement for mimetype check attach_mimetype = env['ir.attachment'].search_read(domain=[('res_model', '=', model), ('res_id', '=', id), ('res_field', '=', field)], fields=['mimetype'], limit=1) mimetype = attach_mimetype and attach_mimetype[0]['mimetype'] if not mimetype: mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype) headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')] # cache etag = bool(request) and request.httprequest.headers.get('If-None-Match') retag = '"%s"' % hashlib.md5(pycompat.to_text(content).encode('utf-8')).hexdigest() status = status or (304 if etag == retag else 200) headers.append(('ETag', retag)) headers.append(('Cache-Control', 'max-age=%s' % (STATIC_CACHE if unique else 0))) # content-disposition default name if download: headers.append(('Content-Disposition', cls.content_disposition(filename))) return (status, headers, content)
def _special_access_object(res_model, res_id, token='', token_field=''): record = request.env[res_model].browse(res_id).sudo() if token and record and getattr(record, token_field, None) and consteq(getattr(record, token_field), token): return True return False
def _check_token(cls, token): base_link = request.httprequest.path params = dict(request.params) params.pop('token', '') valid_token = request.env['mail.thread']._generate_notification_token(base_link, params) return consteq(valid_token, str(token))