Project

General

Profile

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

root / auquotidien / modules / root.py @ af815ab7

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
        return OldRegisterDirectory._q_traverse(self, path)
79

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

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

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

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

    
97

    
98
OldLoginDirectory = wcs.root.LoginDirectory
99

    
100

    
101
class AlternateLoginDirectory(OldLoginDirectory):
102
    def _q_traverse(self, path):
103
        return OldLoginDirectory._q_traverse(self, path)
104

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

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

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

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

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

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

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

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

    
165
            r += htmltext('</div>')
166

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

    
183
            r += form.render()
184
            r += htmltext('</div>')
185

    
186
            get_request().environ['REQUEST_METHOD'] = 'GET'
187

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

    
197

    
198
OldIdentDirectory = wcs.root.IdentDirectory
199

    
200

    
201
class AlternateIdentDirectory(OldIdentDirectory):
202
    def _q_traverse(self, path):
203
        return OldIdentDirectory._q_traverse(self, path)
204

    
205

    
206
class AlternatePreviewDirectory(PreviewDirectory):
207
    def _q_traverse(self, path):
208
        return super(AlternatePreviewDirectory, self)._q_traverse(path)
209

    
210

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

    
243
    register = AlternateRegisterDirectory()
244
    login = AlternateLoginDirectory()
245
    ident = AlternateIdentDirectory()
246
    saml = Saml2Directory()
247
    code = wcs.forms.root.TrackingCodesDirectory()
248
    preview = AlternatePreviewDirectory()
249

    
250
    def get_substitution_variables(self):
251
        return {'links': ''}
252

    
253
    def _q_traverse(self, path):
254
        self.feed_substitution_parts()
255

    
256
        response = get_response()
257
        if not hasattr(response, 'filter'):
258
            response.filter = {}
259

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

    
266
        response.breadcrumb = [('', _('Home'))]
267

    
268
        if not self.admin:
269
            self.admin = get_publisher().admin_directory_class()
270

    
271
        if not self.backoffice:
272
            self.backoffice = get_publisher().backoffice_directory_class()
273

    
274
        return super()._q_traverse(path)
275

    
276
    def json(self):
277
        return FormsRootDirectory().json()
278

    
279
    def categories(self):
280
        return FormsRootDirectory().categories()
281

    
282
    def _q_index(self):
283
        if get_request().is_json():
284
            return FormsRootDirectory().json()
285

    
286
        root_url = get_publisher().get_root_url()
287
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
288
            return redirect('%smyspace/new' % root_url)
289

    
290
        redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
291
        if redirect_url:
292
            return redirect(
293
                misc.get_variadic_url(redirect_url, get_publisher().substitutions.get_context_variables())
294
            )
295

    
296
        template.html_top()
297
        r = TemplateIO(html=True)
298
        get_response().filter['is_index'] = True
299

    
300
        r += htmltext('<div id="centre">')
301
        r += self.box_services(position='1st')
302
        r += htmltext('</div>')
303
        r += htmltext('<div id="droite">')
304
        r += self.myspace_snippet()
305
        r += self.box_services(position='2nd')
306
        r += self.consultations()
307
        r += htmltext('</div>')
308

    
309
        user = get_request().user
310
        if user and user.can_go_in_backoffice():
311
            get_response().filter['backoffice'] = True
312

    
313
        return r.getvalue()
314

    
315
    def services(self):
316
        template.html_top()
317
        return self.box_services(level=2)
318

    
319
    def box_services(self, level=3, position=None):
320
        ## Services
321
        if get_request().user and get_request().user.roles:
322
            accepted_roles = get_request().user.roles
323
        else:
324
            accepted_roles = []
325

    
326
        cats = Category.select(order_by='name')
327
        cats = [x for x in cats if x.url_name != 'consultations']
328
        Category.sort_by_position(cats)
329

    
330
        all_formdefs = FormDef.select(
331
            lambda x: not x.is_disabled() or x.disabled_redirection, order_by='name'
332
        )
333

    
334
        if position:
335
            t = self.display_list_of_formdefs(
336
                [x for x in cats if x.get_homepage_position() == position], all_formdefs, accepted_roles
337
            )
338
        else:
339
            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
340

    
341
        if not t:
342
            return
343

    
344
        r = TemplateIO(html=True)
345

    
346
        if position == '2nd':
347
            r += htmltext('<div id="services-2nd">')
348
        else:
349
            r += htmltext('<div id="services">')
350
        if level == 2:
351
            r += htmltext('<h2>%s</h2>') % _('Services')
352
        else:
353
            r += htmltext('<h3>%s</h3>') % _('Services')
354

    
355
        if 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
356
            homepage_text = TextsDirectory.get_html_text('aq-home-page')
357
            if homepage_text:
358
                r += htmltext('<div id="home-page-intro">')
359
                r += homepage_text
360
                r += htmltext('</div>')
361

    
362
        r += htmltext('<ul>')
363
        r += t
364
        r += htmltext('</ul>')
365

    
366
        r += htmltext('</div>')
367
        return r.getvalue()
368

    
369
    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
370
        r = TemplateIO(html=True)
371
        for category in cats:
372
            if category.url_name == 'consultations':
373
                self.consultations_category = category
374
                continue
375
            formdefs = [x for x in all_formdefs if str(x.category_id) == str(category.id)]
376
            formdefs_advertise = []
377

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

    
398
            if not formdefs and not formdefs_advertise:
399
                continue
400

    
401
            keywords = {}
402
            for formdef in formdefs:
403
                for keyword in formdef.keywords_list:
404
                    keywords[keyword] = True
405

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

    
448
        return r.getvalue()
449

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

    
482
    def box_side(self, path):
483
        r = TemplateIO(html=True)
484
        root_url = get_publisher().get_root_url()
485

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

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

    
509
        v = r.getvalue()
510
        if v:
511
            r = TemplateIO(html=True)
512
            r += htmltext('<div id="sidebox">')
513
            r += v
514
            r += htmltext('</div>')
515
            return r.getvalue()
516

    
517
        return None
518

    
519
    def has_anonymous_access_codes(self):
520
        return any((x for x in FormDef.select() if x.enable_tracking_codes))
521

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

    
539

    
540
from qommon.publisher import get_publisher_class
541

    
542
get_publisher_class().root_directory_class = AlternateRootDirectory
543
get_publisher_class().after_login_url = 'myspace/'
544
get_publisher_class().use_sms_feature = True
545

    
546

    
547
TextsDirectory.register(
548
    'aq-sso-text',
549
    N_('Connecting with Identity Provider'),
550
    default=N_(
551
        '''<h3>Connecting with Identity Provider</h3>
552
<p>You can also use your identity provider to connect.
553
</p>'''
554
    ),
555
)
556

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