Project

General

Profile

Download (19.3 KB) Statistics
| Branch: | Tag: | Revision:

root / auquotidien / modules / root.py @ fd36f89b

1
import urllib.parse
2

    
3
from quixote import get_publisher, get_response, get_request, redirect, get_session
4
from quixote.directory import Directory
5
from quixote.html import TemplateIO, htmltext
6

    
7
from wcs.qommon import _
8
from wcs.qommon.misc import get_variadic_url, simplify
9

    
10
import os
11
import re
12
import string
13

    
14
try:
15
    import lasso
16
except ImportError:
17
    pass
18

    
19
import wcs
20
import wcs.root
21
from wcs import qommon
22
from wcs.forms.root import RootDirectory as FormsRootDirectory
23
from wcs.qommon import N_, get_cfg, get_logger
24
from wcs.qommon import template
25
from wcs.qommon import errors
26
from wcs.qommon.form import *
27
from wcs.qommon import logger
28
from wcs.roles import logged_users_role
29

    
30
from wcs.qommon import emails
31
from wcs.qommon.sms import SMS
32
from wcs.categories import Category
33
from wcs.formdef import FormDef
34
from wcs.data_sources import NamedDataSource
35
from wcs.qommon.tokens import Token
36
from wcs.qommon.admin.emails import EmailsDirectory
37
from wcs.qommon.admin.texts import TextsDirectory
38

    
39
from .myspace import MyspaceDirectory
40

    
41
import wcs.forms.root
42
from wcs.workflows import Workflow
43
from wcs.forms.preview import PreviewDirectory
44

    
45
from .saml2 import Saml2Directory
46

    
47
OldRootDirectory = wcs.root.RootDirectory
48

    
49
import wcs.qommon.ident.password
50
import wcs.qommon.ident.idp
51

    
52

    
53
def category_get_homepage_position(self):
54
    if hasattr(self, 'homepage_position') and self.homepage_position:
55
        return self.homepage_position
56
    if self.url_name == 'consultations':
57
        return '2nd'
58
    return '1st'
59

    
60

    
61
Category.get_homepage_position = category_get_homepage_position
62

    
63

    
64
def category_get_limit(self):
65
    if hasattr(self, 'limit') and self.limit is not None:
66
        return self.limit
67
    return 7
68

    
69

    
70
Category.get_limit = category_get_limit
71

    
72
Category.TEXT_ATTRIBUTES = ['name', 'url_name', 'description', 'homepage_position']
73
Category.INT_ATTRIBUTES = ['position', 'limit']
74

    
75
OldRegisterDirectory = wcs.root.RegisterDirectory
76

    
77

    
78
class AlternateRegisterDirectory(OldRegisterDirectory):
79
    def _q_traverse(self, path):
80
        get_response().filter['bigdiv'] = 'new_member'
81
        return OldRegisterDirectory._q_traverse(self, path)
82

    
83
    def _q_index(self):
84
        get_logger().info('register')
85
        ident_methods = get_cfg('identification', {}).get('methods', [])
86

    
87
        if len(ident_methods) == 0:
88
            idps = get_cfg('idp', {})
89
            if len(idps) == 0:
90
                return template.error_page(_('Authentication subsystem is not yet configured.'))
91
            ident_methods = ['idp']  # fallback to old behaviour; saml.
92

    
93
        if len(ident_methods) == 1:
94
            method = ident_methods[0]
95
        else:
96
            method = 'password'
97

    
98
        return wcs.qommon.ident.register(method)
99

    
100

    
101
OldLoginDirectory = wcs.root.LoginDirectory
102

    
103

    
104
class AlternateLoginDirectory(OldLoginDirectory):
105
    def _q_traverse(self, path):
106
        get_response().filter['bigdiv'] = 'member'
107
        return OldLoginDirectory._q_traverse(self, path)
108

    
109
    def _q_index(self):
110
        get_logger().info('login')
111
        ident_methods = get_cfg('identification', {}).get('methods', [])
112

    
113
        if get_request().form.get('ReturnUrl'):
114
            get_request().form['next'] = get_request().form.pop('ReturnUrl')
115

    
116
        if 'IsPassive' in get_request().form and 'idp' in ident_methods:
117
            # if isPassive is given in query parameters, we restrict ourselves
118
            # to saml login.
119
            ident_methods = ['idp']
120

    
121
        if len(ident_methods) > 1 and 'idp' in ident_methods:
122
            # if there is more than one identification method, and there is a
123
            # possibility of SSO, if we got there as a consequence of an access
124
            # unauthorized url on admin/ or backoffice/, then idp auth method
125
            # is chosen forcefully.
126
            after_url = get_request().form.get('next')
127
            if after_url:
128
                root_url = get_publisher().get_root_url()
129
                after_path = urllib.parse.urlparse(after_url)[2]
130
                after_path = after_path[len(root_url) :]
131
                if after_path.startswith(str('admin')) or after_path.startswith(str('backoffice')):
132
                    ident_methods = ['idp']
133

    
134
        # don't display authentication system choice
135
        if len(ident_methods) == 1:
136
            method = ident_methods[0]
137
            try:
138
                return wcs.qommon.ident.login(method)
139
            except KeyError:
140
                get_logger().error('failed to login with method %s' % method)
141
                return errors.TraversalError()
142

    
143
        if sorted(ident_methods) == ['idp', 'password']:
144
            r = TemplateIO(html=True)
145
            get_response().breadcrumb.append(('login', _('Login')))
146
            identities_cfg = get_cfg('identities', {})
147
            form = Form(enctype='multipart/form-data', id='login-form', use_tokens=False)
148
            if identities_cfg.get('email-as-username', False):
149
                form.add(StringWidget, 'username', title=_('Email'), size=25, required=True)
150
            else:
151
                form.add(StringWidget, 'username', title=_('Username'), size=25, required=True)
152
            form.add(PasswordWidget, 'password', title=_('Password'), size=25, required=True)
153
            form.add_submit('submit', _('Connect'))
154
            if form.is_submitted() and not form.has_errors():
155
                tmp = wcs.qommon.ident.password.MethodDirectory().login_submit(form)
156
                if not form.has_errors():
157
                    return tmp
158

    
159
            r += htmltext('<div id="login-password">')
160
            r += get_session().display_message()
161
            r += form.render()
162

    
163
            base_url = get_publisher().get_root_url()
164
            r += htmltext('<p><a href="%sident/password/forgotten">%s</a></p>') % (
165
                base_url,
166
                _('Forgotten password ?'),
167
            )
168

    
169
            r += htmltext('</div>')
170

    
171
            # XXX: this part only supports a single IdP
172
            r += htmltext('<div id="login-sso">')
173
            r += TextsDirectory.get_html_text('aq-sso-text')
174
            form = Form(enctype='multipart/form-data', action='%sident/idp/login' % base_url)
175
            form.add_hidden('method', 'idp')
176
            for kidp, idp in get_cfg('idp', {}).items():
177
                p = lasso.Provider(
178
                    lasso.PROVIDER_ROLE_IDP,
179
                    misc.get_abs_path(idp['metadata']),
180
                    misc.get_abs_path(idp.get('publickey')),
181
                    None,
182
                )
183
                form.add_hidden('idp', p.providerId)
184
                break
185
            form.add_submit('submit', _('Connect'))
186

    
187
            r += form.render()
188
            r += htmltext('</div>')
189

    
190
            get_request().environ['REQUEST_METHOD'] = 'GET'
191

    
192
            r += htmltext(
193
                """<script type="text/javascript">
194
              document.getElementById('login-form')['username'].focus();
195
            </script>"""
196
            )
197
            return r.getvalue()
198
        else:
199
            return OldLoginDirectory._q_index(self)
200

    
201

    
202
OldIdentDirectory = wcs.root.IdentDirectory
203

    
204

    
205
class AlternateIdentDirectory(OldIdentDirectory):
206
    def _q_traverse(self, path):
207
        get_response().filter['bigdiv'] = 'member'
208
        return OldIdentDirectory._q_traverse(self, path)
209

    
210

    
211
class AlternatePreviewDirectory(PreviewDirectory):
212
    def _q_traverse(self, path):
213
        get_response().filter['bigdiv'] = 'rub_service'
214
        return super(AlternatePreviewDirectory, self)._q_traverse(path)
215

    
216

    
217
class AlternateRootDirectory(OldRootDirectory):
218
    _q_exports = [
219
        '',
220
        'admin',
221
        'backoffice',
222
        'forms',
223
        'login',
224
        'logout',
225
        'saml',
226
        'register',
227
        'ident',
228
        'afterjobs',
229
        'myspace',
230
        'services',
231
        'categories',
232
        'user',
233
        ('tmp-upload', 'tmp_upload'),
234
        'json',
235
        '__version__',
236
        'roles',
237
        'api',
238
        'code',
239
        'fargo',
240
        'tryauth',
241
        'auth',
242
        'preview',
243
        ('reload-top', 'reload_top'),
244
        'static',
245
        ('i18n.js', 'i18n_js'),
246
        'actions',
247
    ]
248

    
249
    register = AlternateRegisterDirectory()
250
    login = AlternateLoginDirectory()
251
    ident = AlternateIdentDirectory()
252
    myspace = MyspaceDirectory()
253
    saml = Saml2Directory()
254
    code = wcs.forms.root.TrackingCodesDirectory()
255
    preview = AlternatePreviewDirectory()
256

    
257
    def get_substitution_variables(self):
258
        return {'links': ''}
259

    
260
    def _q_traverse(self, path):
261
        self.feed_substitution_parts()
262

    
263
        response = get_response()
264
        if not hasattr(response, 'filter'):
265
            response.filter = {}
266

    
267
        response.filter['auquotidien'] = True
268
        if not path or (path[0] not in ('api', 'backoffice') and not get_request().is_json()):
269
            # api & backoffice have no use for a side box
270
            response.filter['gauche'] = lambda x: self.box_side(path)
271
        get_publisher().substitutions.feed(self)
272

    
273
        response.breadcrumb = [('', _('Home'))]
274

    
275
        if not self.admin:
276
            self.admin = get_publisher().admin_directory_class()
277

    
278
        if not self.backoffice:
279
            self.backoffice = get_publisher().backoffice_directory_class()
280

    
281
        return super()._q_traverse(path)
282

    
283
    def json(self):
284
        return FormsRootDirectory().json()
285

    
286
    def categories(self):
287
        return FormsRootDirectory().categories()
288

    
289
    def _q_index(self):
290
        if get_request().is_json():
291
            return FormsRootDirectory().json()
292

    
293
        root_url = get_publisher().get_root_url()
294
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
295
            return redirect('%smyspace/new' % root_url)
296

    
297
        redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
298
        if redirect_url:
299
            return redirect(
300
                misc.get_variadic_url(redirect_url, get_publisher().substitutions.get_context_variables())
301
            )
302

    
303
        template.html_top()
304
        r = TemplateIO(html=True)
305
        get_response().filter['is_index'] = True
306

    
307
        r += htmltext('<div id="centre">')
308
        r += self.box_services(position='1st')
309
        r += htmltext('</div>')
310
        r += htmltext('<div id="droite">')
311
        r += self.myspace_snippet()
312
        r += self.box_services(position='2nd')
313
        r += self.consultations()
314
        r += htmltext('</div>')
315

    
316
        user = get_request().user
317
        if user and user.can_go_in_backoffice():
318
            get_response().filter['backoffice'] = True
319

    
320
        return r.getvalue()
321

    
322
    def services(self):
323
        template.html_top()
324
        get_response().filter['bigdiv'] = 'rub_service'
325
        return self.box_services(level=2)
326

    
327
    def box_services(self, level=3, position=None):
328
        ## Services
329
        if get_request().user and get_request().user.roles:
330
            accepted_roles = get_request().user.roles
331
        else:
332
            accepted_roles = []
333

    
334
        cats = Category.select(order_by='name')
335
        cats = [x for x in cats if x.url_name != 'consultations']
336
        Category.sort_by_position(cats)
337

    
338
        all_formdefs = FormDef.select(
339
            lambda x: not x.is_disabled() or x.disabled_redirection, order_by='name'
340
        )
341

    
342
        if position:
343
            t = self.display_list_of_formdefs(
344
                [x for x in cats if x.get_homepage_position() == position], all_formdefs, accepted_roles
345
            )
346
        else:
347
            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
348

    
349
        if not t:
350
            return
351

    
352
        r = TemplateIO(html=True)
353

    
354
        if position == '2nd':
355
            r += htmltext('<div id="services-2nd">')
356
        else:
357
            r += htmltext('<div id="services">')
358
        if level == 2:
359
            r += htmltext('<h2>%s</h2>') % _('Services')
360
        else:
361
            r += htmltext('<h3>%s</h3>') % _('Services')
362

    
363
        if 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
364
            homepage_text = TextsDirectory.get_html_text('aq-home-page')
365
            if homepage_text:
366
                r += htmltext('<div id="home-page-intro">')
367
                r += homepage_text
368
                r += htmltext('</div>')
369

    
370
        r += htmltext('<ul>')
371
        r += t
372
        r += htmltext('</ul>')
373

    
374
        r += htmltext('</div>')
375
        return r.getvalue()
376

    
377
    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
378
        r = TemplateIO(html=True)
379
        for category in cats:
380
            if category.url_name == 'consultations':
381
                self.consultations_category = category
382
                continue
383
            formdefs = [x for x in all_formdefs if str(x.category_id) == str(category.id)]
384
            formdefs_advertise = []
385

    
386
            for formdef in formdefs[:]:
387
                if formdef.is_disabled():  # is a redirection
388
                    continue
389
                if not formdef.roles:
390
                    continue
391
                if not get_request().user:
392
                    if formdef.always_advertise:
393
                        formdefs_advertise.append(formdef)
394
                    formdefs.remove(formdef)
395
                    continue
396
                if logged_users_role().id in formdef.roles:
397
                    continue
398
                for q in accepted_roles:
399
                    if q in formdef.roles:
400
                        break
401
                else:
402
                    if formdef.always_advertise:
403
                        formdefs_advertise.append(formdef)
404
                    formdefs.remove(formdef)
405

    
406
            if not formdefs and not formdefs_advertise:
407
                continue
408

    
409
            keywords = {}
410
            for formdef in formdefs:
411
                for keyword in formdef.keywords_list:
412
                    keywords[keyword] = True
413

    
414
            r += htmltext('<li id="category-%s" data-keywords="%s">') % (
415
                category.url_name,
416
                ' '.join(keywords),
417
            )
418
            r += htmltext('<strong>')
419
            r += htmltext('<a href="%s/">') % category.url_name
420
            r += category.name
421
            r += htmltext('</a></strong>\n')
422
            r += category.get_description_html_text()
423
            r += htmltext('<ul>')
424
            limit = category.get_limit()
425
            for formdef in formdefs[:limit]:
426
                r += htmltext('<li data-keywords="%s">') % ' '.join(formdef.keywords_list)
427
                classes = []
428
                if formdef.is_disabled() and formdef.disabled_redirection:
429
                    classes.append('redirection')
430
                r += htmltext('<a class="%s" href="%s/%s/">%s</a>') % (
431
                    ' '.join(classes),
432
                    category.url_name,
433
                    formdef.url_name,
434
                    formdef.name,
435
                )
436
                r += htmltext('</li>\n')
437
            if len(formdefs) < limit:
438
                for formdef in formdefs_advertise[: limit - len(formdefs)]:
439
                    r += htmltext('<li class="required-authentication">')
440
                    r += htmltext('<a href="%s/%s/">%s</a>') % (
441
                        category.url_name,
442
                        formdef.url_name,
443
                        formdef.name,
444
                    )
445
                    r += htmltext('<span> (%s)</span>') % _('authentication required')
446
                    r += htmltext('</li>\n')
447
            if (len(formdefs) + len(formdefs_advertise)) > limit:
448
                r += htmltext('<li class="all-forms"><a href="%s/" title="%s">%s</a></li>') % (
449
                    category.url_name,
450
                    _('Access to all forms of the "%s" category') % category.name,
451
                    _('Access to all forms in this category'),
452
                )
453
            r += htmltext('</ul>')
454
            r += htmltext('</li>\n')
455

    
456
        return r.getvalue()
457

    
458
    def consultations(self):
459
        cats = [x for x in Category.select() if x.url_name == 'consultations']
460
        if not cats:
461
            return
462
        consultations_category = cats[0]
463
        formdefs = FormDef.select(
464
            lambda x: (
465
                str(x.category_id) == str(consultations_category.id)
466
                and (not x.is_disabled() or x.disabled_redirection)
467
            ),
468
            order_by='name',
469
        )
470
        if not formdefs:
471
            return
472
        ## Consultations
473
        r = TemplateIO(html=True)
474
        r += htmltext('<div id="consultations">')
475
        r += htmltext('<h3>%s</h3>') % _('Consultations')
476
        r += consultations_category.get_description_html_text()
477
        r += htmltext('<ul>')
478
        for formdef in formdefs:
479
            r += htmltext('<li>')
480
            r += htmltext('<a href="%s/%s/">%s</a>') % (
481
                consultations_category.url_name,
482
                formdef.url_name,
483
                formdef.name,
484
            )
485
            r += htmltext('</li>')
486
        r += htmltext('</ul>')
487
        r += htmltext('</div>')
488
        return r.getvalue()
489

    
490
    def box_side(self, path):
491
        r = TemplateIO(html=True)
492
        root_url = get_publisher().get_root_url()
493

    
494
        if (
495
            path == ['']
496
            and 'include-tracking-code-form' in get_response().filter.get('keywords', [])
497
            and self.has_anonymous_access_codes()
498
        ):
499
            r += htmltext('<form id="follow-form" action="%scode/load">') % root_url
500
            r += htmltext('<h3>%s</h3>') % _('Tracking code')
501
            r += htmltext('<input size="12" name="code" placeholder="%s"/>') % _('ex: RPQDFVCD')
502
            r += htmltext('<input type="submit" value="%s"/>') % _('Load')
503
            r += htmltext('</form>')
504

    
505
        cats = Category.select(order_by='name')
506
        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
507
        Category.sort_by_position(cats)
508
        if cats:
509
            r += htmltext('<div id="side-services">')
510
            r += htmltext('<h3>%s</h3>') % _('Services')
511
            r += htmltext('<ul>')
512
            for cat in cats:
513
                r += htmltext('<li><a href="%s/">%s</a></li>') % (cat.url_name, cat.name)
514
            r += htmltext('</ul>')
515
            r += htmltext('</div>')
516

    
517
        v = r.getvalue()
518
        if v:
519
            r = TemplateIO(html=True)
520
            r += htmltext('<div id="sidebox">')
521
            r += v
522
            r += htmltext('</div>')
523
            return r.getvalue()
524

    
525
        return None
526

    
527
    def has_anonymous_access_codes(self):
528
        return any((x for x in FormDef.select() if x.enable_tracking_codes))
529

    
530
    def myspace_snippet(self):
531
        r = TemplateIO(html=True)
532
        r += htmltext('<div id="myspace">')
533
        r += htmltext('<h3>%s</h3>') % _('My Space')
534
        r += htmltext('<ul>')
535
        if get_request().user and not get_request().user.anonymous:
536
            r += htmltext('  <li><a href="myspace/" id="member">%s</a></li>') % _(
537
                'Access to your personal space'
538
            )
539
            r += htmltext('  <li><a href="logout" id="logout">%s</a></li>') % _('Logout')
540
        else:
541
            r += htmltext('  <li><a href="register/" id="inscr">%s</a></li>') % _('Registration')
542
            r += htmltext('  <li><a href="login/" id="login">%s</a></li>') % _('Login')
543
        r += htmltext('</ul>')
544
        r += htmltext('</div>')
545
        return r.getvalue()
546

    
547

    
548
from qommon.publisher import get_publisher_class
549

    
550
get_publisher_class().root_directory_class = AlternateRootDirectory
551
get_publisher_class().after_login_url = 'myspace/'
552
get_publisher_class().use_sms_feature = True
553

    
554

    
555
TextsDirectory.register(
556
    'aq-sso-text',
557
    N_('Connecting with Identity Provider'),
558
    default=N_(
559
        '''<h3>Connecting with Identity Provider</h3>
560
<p>You can also use your identity provider to connect.
561
</p>'''
562
    ),
563
)
564

    
565
TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg=True)
(9-9/11)