[IMP] portal: sanitize email address when using it as a user login
[odoo/odoo.git] / addons / portal / wizard / portal_wizard.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import logging
23 import random
24
25 from osv import osv, fields
26 from tools.translate import _
27 from tools.misc import email_re
28 from openerp import SUPERUSER_ID
29
30 from base.res.res_partner import _lang_get
31 _logger = logging.getLogger(__name__)
32
33 # welcome email sent to new portal users (note that calling tools.translate._
34 # has no effect except exporting those strings for translation)
35 WELCOME_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s")
36 WELCOME_EMAIL_BODY = _("""Dear %(name)s,
37
38 You have been created an OpenERP account of %(portal)s at %(url)s.
39
40 Your login account data is:
41 Database: %(db)s
42 User:     %(login)s
43 Password: %(password)s
44
45 %(message)s
46
47 --
48 OpenERP - Open Source Business Applications
49 http://www.openerp.com
50 """)
51
52 # character sets for passwords, excluding 0, O, o, 1, I, l
53 _PASSU = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'
54 _PASSL = 'abcdefghijkmnpqrstuvwxyz'
55 _PASSD = '23456789'
56
57 def random_password():
58     # get 3 uppercase letters, 3 lowercase letters, 2 digits, and shuffle them
59     chars = map(random.choice, [_PASSU] * 3 + [_PASSL] * 3 + [_PASSD] * 2)
60     random.shuffle(chars)
61     return ''.join(chars)
62
63 def extract_email(email):
64     """ extract the email address from a user-friendly email address """
65     m = email_re.search(email or "")
66     return m and m.group(0) or ""
67
68
69
70 class wizard(osv.osv_memory):
71     """
72         A wizard to manage the creation/removal of portal users.
73     """
74     _name = 'portal.wizard'
75     _description = 'Portal Access Management'
76
77     _columns = {
78         'portal_id': fields.many2one('res.groups', domain=[('is_portal', '=', True)], required=True,
79             string='Portal', help="The portal that users can be added in or removed from."),
80         'user_ids': fields.one2many('portal.wizard.user', 'wizard_id', string='Users'),
81         'message': fields.text(string='Invitation Message',
82             help="This text is included in the welcome email sent to the users."),
83     }
84
85     def _default_portal(self, cr, uid, context):
86         portal_ids = self.pool.get('res.groups').search(cr, uid, [('is_portal', '=', True)])
87         return portal_ids and portal_ids[0] or False
88
89     _defaults = {
90         'portal_id': _default_portal,
91     }
92
93     def onchange_portal_id(self, cr, uid, ids, portal_id, context=None):
94         # for each partner, determine corresponding portal.wizard.user records
95         res_partner = self.pool.get('res.partner')
96         partner_ids = context and context.get('active_ids') or []
97         user_changes = []
98         for partner in res_partner.browse(cr, SUPERUSER_ID, partner_ids, context):
99             for contact in (partner.child_ids or [partner]):
100                 in_portal = False
101                 if contact.user_ids:
102                     in_portal = portal_id in [g.id for g in contact.user_ids[0].groups_id]
103                 user_changes.append((0, 0, {
104                     'partner_id': contact.id,
105                     'email': contact.email,
106                     'in_portal': in_portal,
107                 }))
108         return {'value': {'user_ids': user_changes}}
109
110     def action_apply(self, cr, uid, ids, context=None):
111         wizard = self.browse(cr, uid, ids[0], context)
112         portal_user_ids = [user.id for user in wizard.user_ids]
113         self.pool.get('portal.wizard.user').action_apply(cr, uid, portal_user_ids, context)
114         return {'type': 'ir.actions.act_window_close'}
115
116 class wizard_user(osv.osv_memory):
117     """
118         A model to configure users in the portal wizard.
119     """
120     _name = 'portal.wizard.user'
121     _description = 'Portal User Config'
122
123     _columns = {
124         'wizard_id': fields.many2one('portal.wizard', string='Wizard', required=True),
125         'partner_id': fields.many2one('res.partner', string='Contact', required=True, readonly=True),
126         'email': fields.related('partner_id', 'email', type='char', string='Email'),
127         'in_portal': fields.boolean('In Portal'),
128     }
129
130     def action_apply(self, cr, uid, ids, context=None):
131         res_users = self.pool.get('res.users')
132         for wizard_user in self.browse(cr, SUPERUSER_ID, ids, context):
133             portal = wizard_user.wizard_id.portal_id
134             user = self._retrieve_user(cr, SUPERUSER_ID, wizard_user, context)
135             if wizard_user.in_portal:
136                 # create a user if necessary, and make sure it is in the portal group
137                 if not user:
138                     user = self._create_user(cr, SUPERUSER_ID, wizard_user, context)
139                 if (not user.active) or (portal not in user.groups_id):
140                     user.write({'active': True, 'groups_id': [(4, portal.id)]})
141                     wizard_user = self.browse(cr, SUPERUSER_ID, wizard_user.id, context)
142                     self._send_email(cr, uid, wizard_user, context)
143             else:
144                 # remove the user (if it exists) from the portal group
145                 if user:
146                     if portal in user.groups_id:
147                         values = {'groups_id': [(3, portal.id)]}
148                         if len(user.groups_id) == 1:
149                             values['active'] = False            # deactivate user
150                         user.write(values)
151
152     def _retrieve_user(self, cr, uid, wizard_user, context=None):
153         """ retrieve the (possibly inactive) user corresponding to wizard_user.partner_id
154             @param wizard_user: browse record of model portal.wizard.user
155             @return: browse record of model res.users
156         """
157         if wizard_user.partner_id.user_ids:
158             return wizard_user.partner_id.user_ids[0]
159         # the user may be inactive, search for it
160         res_users = self.pool.get('res.users')
161         domain = [('partner_id', '=', wizard_user.partner_id.id), ('active', '=', False)]
162         user_ids = res_users.search(cr, uid, domain)
163         return user_ids and res_users.browse(cr, uid, user_ids[0], context) or False
164
165     def _create_user(self, cr, uid, wizard_user, context=None):
166         """ create a new user for wizard_user.partner_id
167             @param wizard_user: browse record of model portal.wizard.user
168             @return: browse record of model res.users
169         """
170         res_users = self.pool.get('res.users')
171         create_context = dict(context or {}, noshortcut=True)       # to prevent shortcut creation
172         values = {
173             'login': extract_email(wizard_user.email),
174             'password': random_password(),
175             'partner_id': wizard_user.partner_id.id,
176             'groups_id': [(6, 0, [])],
177             'share': True,
178         }
179         user_id = res_users.create(cr, uid, values, context=create_context)
180         return res_users.browse(cr, uid, user_id, context)
181
182     def _send_email(self, cr, uid, wizard_user, context=None):
183         """ send invitation email to a new portal user
184             @param wizard_user: browse record of model portal.wizard.user
185             @return: the id of the created mail.mail record
186         """
187         this_context = context
188         this_user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context)
189         if not this_user.email:
190             raise osv.except_osv(_('Email required'),
191                 _('You must have an email address in your User Preferences to send emails.'))
192
193         # determine subject and body in the portal user's language
194         url = self.pool.get('ir.config_parameter').get_param(cr, SUPERUSER_ID, 'web.base.url', context=this_context)
195         user = wizard_user.partner_id.user_ids[0]
196         context = dict(this_context or {}, lang=user.lang)
197         data = {
198             'company': this_user.company_id.name,
199             'portal': wizard_user.wizard_id.portal_id.name,
200             'message': wizard_user.wizard_id.message or "",
201             'url': url or _("(missing url)"),
202             'db': cr.dbname,
203             'login': user.login,
204             'password': user.password,
205             'name': user.name            
206         }
207         subject = _(WELCOME_EMAIL_SUBJECT) % data
208         body = _(WELCOME_EMAIL_BODY) % data
209
210         mail_mail = self.pool.get('mail.mail')
211         mail_values = {
212             'email_from': this_user.email,
213             'email_to': user.email,
214             'subject': subject,
215             'body_html': '<pre>%s</pre>' % body,
216             'state': 'outgoing',
217         }
218         return mail_mail.create(cr, uid, mail_values, context=this_context)
219
220 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: