Project

General

Profile

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

root / auquotidien / modules / root.py @ 721bd8e5

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
import wcs.forms.root
40
from wcs.workflows import Workflow
41
from wcs.forms.preview import PreviewDirectory
42

    
43
from .saml2 import Saml2Directory
44

    
45
OldRootDirectory = wcs.root.RootDirectory
46

    
47
import wcs.qommon.ident.password
48
import wcs.qommon.ident.idp
49

    
50

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

    
58

    
59
Category.get_homepage_position = category_get_homepage_position
60

    
61

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

    
67

    
68
Category.get_limit = category_get_limit
69

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

    
73
OldRegisterDirectory = wcs.root.RegisterDirectory
74

    
75

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

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

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

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

    
96
        return wcs.qommon.ident.register(method)
97

    
98

    
99
OldLoginDirectory = wcs.root.LoginDirectory
100

    
101

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

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

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

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

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

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

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

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

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

    
167
            r += htmltext('</div>')
168

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

    
185
            r += form.render()
186
            r += htmltext('</div>')
187

    
188
            get_request().environ['REQUEST_METHOD'] = 'GET'
189

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

    
199

    
200
OldIdentDirectory = wcs.root.IdentDirectory
201

    
202

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

    
208

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

    
214

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

    
247
    register = AlternateRegisterDirectory()
248
    login = AlternateLoginDirectory()
249
    ident = AlternateIdentDirectory()
250
    saml = Saml2Directory()
251
    code = wcs.forms.root.TrackingCodesDirectory()
252
    preview = AlternatePreviewDirectory()
253

    
254
    def get_substitution_variables(self):
255
        return {'links': ''}
256

    
257
    def _q_traverse(self, path):
258
        self.feed_substitution_parts()
259

    
260
        response = get_response()
261
        if not hasattr(response, 'filter'):
262
            response.filter = {}
263

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

    
270
        response.breadcrumb = [('', _('Home'))]
271

    
272
        if not self.admin:
273
            self.admin = get_publisher().admin_directory_class()
274

    
275
        if not self.backoffice:
276
            self.backoffice = get_publisher().backoffice_directory_class()
277

    
278
        return super()._q_traverse(path)
279

    
280
    def json(self):
281
        return FormsRootDirectory().json()
282

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

    
286
    def _q_index(self):
287
        if get_request().is_json():
288
            return FormsRootDirectory().json()
289

    
290
        root_url = get_publisher().get_root_url()
291
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
292
            return redirect('%smyspace/new' % root_url)
293

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

    
300
        template.html_top()
301
        r = TemplateIO(html=True)
302
        get_response().filter['is_index'] = True
303

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

    
313
        user = get_request().user
314
        if user and user.can_go_in_backoffice():
315
            get_response().filter['backoffice'] = True
316

    
317
        return r.getvalue()
318

    
319
    def services(self):
320
        template.html_top()
321
        get_response().filter['bigdiv'] = 'rub_service'
322
        return self.box_services(level=2)
323

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

    
331
        cats = Category.select(order_by='name')
332
        cats = [x for x in cats if x.url_name != 'consultations']
333
        Category.sort_by_position(cats)
334

    
335
        all_formdefs = FormDef.select(
336
            lambda x: not x.is_disabled() or x.disabled_redirection, order_by='name'
337
        )
338

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

    
346
        if not t:
347
            return
348

    
349
        r = TemplateIO(html=True)
350

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

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

    
367
        r += htmltext('<ul>')
368
        r += t
369
        r += htmltext('</ul>')
370

    
371
        r += htmltext('</div>')
372
        return r.getvalue()
373

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

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

    
403
            if not formdefs and not formdefs_advertise:
404
                continue
405

    
406
            keywords = {}
407
            for formdef in formdefs:
408
                for keyword in formdef.keywords_list:
409
                    keywords[keyword] = True
410

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

    
453
        return r.getvalue()
454

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

    
487
    def box_side(self, path):
488
        r = TemplateIO(html=True)
489
        root_url = get_publisher().get_root_url()
490

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

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

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

    
522
        return None
523

    
524
    def has_anonymous_access_codes(self):
525
        return any((x for x in FormDef.select() if x.enable_tracking_codes))
526

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

    
544

    
545
from qommon.publisher import get_publisher_class
546

    
547
get_publisher_class().root_directory_class = AlternateRootDirectory
548
get_publisher_class().after_login_url = 'myspace/'
549
get_publisher_class().use_sms_feature = True
550

    
551

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

    
562
TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg=True)
(8-8/10)