1
2
3
4 """
5 This file is part of the web2py Web Framework Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
6 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
7 """
8
9 import base64
10 import cPickle
11 import datetime
12 import thread
13 import logging
14 import sys
15 import glob
16 import os
17 import re
18 import time
19 import traceback
20 import smtplib
21 import urllib
22 import urllib2
23 import Cookie
24 import cStringIO
25 import ConfigParser
26 import email.utils
27 from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
28
29 from gluon.contenttype import contenttype
30 from gluon.storage import Storage, StorageList, Settings, Messages
31 from gluon.utils import web2py_uuid
32 from gluon.fileutils import read_file, check_credentials
33 from gluon import *
34 from gluon.contrib.autolinks import expand_one
35 from gluon.contrib.markmin.markmin2html import \
36 replace_at_urls, replace_autolinks, replace_components
37 from gluon.dal import Row, Set, Query
38
39 import gluon.serializers as serializers
40
41 try:
42
43 import json as json_parser
44 except ImportError:
45 try:
46
47 import simplejson as json_parser
48 except:
49
50 import gluon.contrib.simplejson as json_parser
51
52 __all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki',
53 'PluginManager', 'fetch', 'geocode', 'prettydate']
54
55
56 logger = logging.getLogger("web2py")
57
58 DEFAULT = lambda: None
59
60
61 -def getarg(position, default=None):
62 args = current.request.args
63 if position < 0 and len(args) >= -position:
64 return args[position]
65 elif position >= 0 and len(args) > position:
66 return args[position]
67 else:
68 return default
69
70
71 -def callback(actions, form, tablename=None):
72 if actions:
73 if tablename and isinstance(actions, dict):
74 actions = actions.get(tablename, [])
75 if not isinstance(actions, (list, tuple)):
76 actions = [actions]
77 [action(form) for action in actions]
78
81 b = []
82 for item in a:
83 if isinstance(item, (list, tuple)):
84 b = b + list(item)
85 else:
86 b.append(item)
87 return b
88
95
103
104
105 -class Mail(object):
106 """
107 Class for configuring and sending emails with alternative text / html
108 body, multiple attachments and encryption support
109
110 Works with SMTP and Google App Engine.
111 """
112
114 """
115 Email attachment
116
117 Arguments:
118
119 payload: path to file or file-like object with read() method
120 filename: name of the attachment stored in message; if set to
121 None, it will be fetched from payload path; file-like
122 object payload must have explicit filename specified
123 content_id: id of the attachment; automatically contained within
124 < and >
125 content_type: content type of the attachment; if set to None,
126 it will be fetched from filename using gluon.contenttype
127 module
128 encoding: encoding of all strings passed to this function (except
129 attachment body)
130
131 Content ID is used to identify attachments within the html body;
132 in example, attached image with content ID 'photo' may be used in
133 html message as a source of img tag <img src="cid:photo" />.
134
135 Examples:
136
137 #Create attachment from text file:
138 attachment = Mail.Attachment('/path/to/file.txt')
139
140 Content-Type: text/plain
141 MIME-Version: 1.0
142 Content-Disposition: attachment; filename="file.txt"
143 Content-Transfer-Encoding: base64
144
145 SOMEBASE64CONTENT=
146
147 #Create attachment from image file with custom filename and cid:
148 attachment = Mail.Attachment('/path/to/file.png',
149 filename='photo.png',
150 content_id='photo')
151
152 Content-Type: image/png
153 MIME-Version: 1.0
154 Content-Disposition: attachment; filename="photo.png"
155 Content-Id: <photo>
156 Content-Transfer-Encoding: base64
157
158 SOMEOTHERBASE64CONTENT=
159 """
160
161 - def __init__(
162 self,
163 payload,
164 filename=None,
165 content_id=None,
166 content_type=None,
167 encoding='utf-8'):
168 if isinstance(payload, str):
169 if filename is None:
170 filename = os.path.basename(payload)
171 payload = read_file(payload, 'rb')
172 else:
173 if filename is None:
174 raise Exception('Missing attachment name')
175 payload = payload.read()
176 filename = filename.encode(encoding)
177 if content_type is None:
178 content_type = contenttype(filename)
179 self.my_filename = filename
180 self.my_payload = payload
181 MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1))
182 self.set_payload(payload)
183 self['Content-Disposition'] = 'attachment; filename="%s"' % filename
184 if not content_id is None:
185 self['Content-Id'] = '<%s>' % content_id.encode(encoding)
186 Encoders.encode_base64(self)
187
188 - def __init__(self, server=None, sender=None, login=None, tls=True):
189 """
190 Main Mail object
191
192 Arguments:
193
194 server: SMTP server address in address:port notation
195 sender: sender email address
196 login: sender login name and password in login:password notation
197 or None if no authentication is required
198 tls: enables/disables encryption (True by default)
199
200 In Google App Engine use:
201
202 server='gae'
203
204 For sake of backward compatibility all fields are optional and default
205 to None, however, to be able to send emails at least server and sender
206 must be specified. They are available under following fields:
207
208 mail.settings.server
209 mail.settings.sender
210 mail.settings.login
211
212 When server is 'logging', email is logged but not sent (debug mode)
213
214 Optionally you can use PGP encryption or X509:
215
216 mail.settings.cipher_type = None
217 mail.settings.gpg_home = None
218 mail.settings.sign = True
219 mail.settings.sign_passphrase = None
220 mail.settings.encrypt = True
221 mail.settings.x509_sign_keyfile = None
222 mail.settings.x509_sign_certfile = None
223 mail.settings.x509_nocerts = False
224 mail.settings.x509_crypt_certfiles = None
225
226 cipher_type : None
227 gpg - need a python-pyme package and gpgme lib
228 x509 - smime
229 gpg_home : you can set a GNUPGHOME environment variable
230 to specify home of gnupg
231 sign : sign the message (True or False)
232 sign_passphrase : passphrase for key signing
233 encrypt : encrypt the message
234 ... x509 only ...
235 x509_sign_keyfile : the signers private key filename (PEM format)
236 x509_sign_certfile: the signers certificate filename (PEM format)
237 x509_nocerts : if True then no attached certificate in mail
238 x509_crypt_certfiles: the certificates file to encrypt the messages
239 with can be a file name or a list of
240 file names (PEM format)
241
242 Examples:
243
244 #Create Mail object with authentication data for remote server:
245 mail = Mail('example.com:25', 'me@example.com', 'me:password')
246 """
247
248 settings = self.settings = Settings()
249 settings.server = server
250 settings.sender = sender
251 settings.login = login
252 settings.tls = tls
253 settings.hostname = None
254 settings.ssl = False
255 settings.cipher_type = None
256 settings.gpg_home = None
257 settings.sign = True
258 settings.sign_passphrase = None
259 settings.encrypt = True
260 settings.x509_sign_keyfile = None
261 settings.x509_sign_certfile = None
262 settings.x509_nocerts = False
263 settings.x509_crypt_certfiles = None
264 settings.debug = False
265 settings.lock_keys = True
266 self.result = {}
267 self.error = None
268
269 - def send(
270 self,
271 to,
272 subject = '[no subject]',
273 message = '[no message]',
274 attachments=None,
275 cc=None,
276 bcc=None,
277 reply_to=None,
278 sender=None,
279 encoding='utf-8',
280 raw=False,
281 headers={},
282 from_address=None
283 ):
284 """
285 Sends an email using data specified in constructor
286
287 Arguments:
288
289 to: list or tuple of receiver addresses; will also accept single
290 object
291 subject: subject of the email
292 message: email body text; depends on type of passed object:
293 if 2-list or 2-tuple is passed: first element will be
294 source of plain text while second of html text;
295 otherwise: object will be the only source of plain text
296 and html source will be set to None;
297 If text or html source is:
298 None: content part will be ignored,
299 string: content part will be set to it,
300 file-like object: content part will be fetched from
301 it using it's read() method
302 attachments: list or tuple of Mail.Attachment objects; will also
303 accept single object
304 cc: list or tuple of carbon copy receiver addresses; will also
305 accept single object
306 bcc: list or tuple of blind carbon copy receiver addresses; will
307 also accept single object
308 reply_to: address to which reply should be composed
309 encoding: encoding of all strings passed to this method (including
310 message bodies)
311 headers: dictionary of headers to refine the headers just before
312 sending mail, e.g. {'X-Mailer' : 'web2py mailer'}
313 from_address: address to appear in the 'From:' header, this is not the
314 envelope sender. If not specified the sender will be used
315 Examples:
316
317 #Send plain text message to single address:
318 mail.send('you@example.com',
319 'Message subject',
320 'Plain text body of the message')
321
322 #Send html message to single address:
323 mail.send('you@example.com',
324 'Message subject',
325 '<html>Plain text body of the message</html>')
326
327 #Send text and html message to three addresses (two in cc):
328 mail.send('you@example.com',
329 'Message subject',
330 ('Plain text body', '<html>html body</html>'),
331 cc=['other1@example.com', 'other2@example.com'])
332
333 #Send html only message with image attachment available from
334 the message by 'photo' content id:
335 mail.send('you@example.com',
336 'Message subject',
337 (None, '<html><img src="cid:photo" /></html>'),
338 Mail.Attachment('/path/to/photo.jpg'
339 content_id='photo'))
340
341 #Send email with two attachments and no body text
342 mail.send('you@example.com,
343 'Message subject',
344 None,
345 [Mail.Attachment('/path/to/fist.file'),
346 Mail.Attachment('/path/to/second.file')])
347
348 Returns True on success, False on failure.
349
350 Before return, method updates two object's fields:
351 self.result: return value of smtplib.SMTP.sendmail() or GAE's
352 mail.send_mail() method
353 self.error: Exception message or None if above was successful
354 """
355
356
357 Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
358
359 def encode_header(key):
360 if [c for c in key if 32 > ord(c) or ord(c) > 127]:
361 return Header.Header(key.encode('utf-8'), 'utf-8')
362 else:
363 return key
364
365
366 def encoded_or_raw(text):
367 if raw:
368 text = encode_header(text)
369 return text
370
371 sender = sender or self.settings.sender
372
373 if not isinstance(self.settings.server, str):
374 raise Exception('Server address not specified')
375 if not isinstance(sender, str):
376 raise Exception('Sender address not specified')
377
378 if not raw and attachments:
379
380 payload_in = MIMEMultipart.MIMEMultipart('mixed')
381 elif raw:
382
383 if not isinstance(message, basestring):
384 message = message.read()
385 if isinstance(message, unicode):
386 text = message.encode('utf-8')
387 elif not encoding == 'utf-8':
388 text = message.decode(encoding).encode('utf-8')
389 else:
390 text = message
391
392
393
394 payload_in = MIMEText.MIMEText(text)
395 if to:
396 if not isinstance(to, (list, tuple)):
397 to = [to]
398 else:
399 raise Exception('Target receiver address not specified')
400 if cc:
401 if not isinstance(cc, (list, tuple)):
402 cc = [cc]
403 if bcc:
404 if not isinstance(bcc, (list, tuple)):
405 bcc = [bcc]
406 if message is None:
407 text = html = None
408 elif isinstance(message, (list, tuple)):
409 text, html = message
410 elif message.strip().startswith('<html') and \
411 message.strip().endswith('</html>'):
412 text = self.settings.server == 'gae' and message or None
413 html = message
414 else:
415 text = message
416 html = None
417
418 if (not text is None or not html is None) and (not raw):
419
420 if not text is None:
421 if not isinstance(text, basestring):
422 text = text.read()
423 if isinstance(text, unicode):
424 text = text.encode('utf-8')
425 elif not encoding == 'utf-8':
426 text = text.decode(encoding).encode('utf-8')
427 if not html is None:
428 if not isinstance(html, basestring):
429 html = html.read()
430 if isinstance(html, unicode):
431 html = html.encode('utf-8')
432 elif not encoding == 'utf-8':
433 html = html.decode(encoding).encode('utf-8')
434
435
436 if text is not None and html:
437
438 attachment = MIMEMultipart.MIMEMultipart('alternative')
439 attachment.attach(MIMEText.MIMEText(text, _charset='utf-8'))
440 attachment.attach(
441 MIMEText.MIMEText(html, 'html', _charset='utf-8'))
442 elif text is not None:
443 attachment = MIMEText.MIMEText(text, _charset='utf-8')
444 elif html:
445 attachment = \
446 MIMEText.MIMEText(html, 'html', _charset='utf-8')
447
448 if attachments:
449
450
451 payload_in.attach(attachment)
452 else:
453
454 payload_in = attachment
455
456 if (attachments is None) or raw:
457 pass
458 elif isinstance(attachments, (list, tuple)):
459 for attachment in attachments:
460 payload_in.attach(attachment)
461 else:
462 payload_in.attach(attachments)
463
464
465
466
467 cipher_type = self.settings.cipher_type
468 sign = self.settings.sign
469 sign_passphrase = self.settings.sign_passphrase
470 encrypt = self.settings.encrypt
471
472
473
474 if cipher_type == 'gpg':
475 if self.settings.gpg_home:
476
477 import os
478 os.environ['GNUPGHOME'] = self.settings.gpg_home
479 if not sign and not encrypt:
480 self.error = "No sign and no encrypt is set but cipher type to gpg"
481 return False
482
483
484 from pyme import core, errors
485 from pyme.constants.sig import mode
486
487
488
489 if sign:
490 import string
491 core.check_version(None)
492 pin = string.replace(payload_in.as_string(), '\n', '\r\n')
493 plain = core.Data(pin)
494 sig = core.Data()
495 c = core.Context()
496 c.set_armor(1)
497 c.signers_clear()
498
499 for sigkey in c.op_keylist_all(sender, 1):
500 if sigkey.can_sign:
501 c.signers_add(sigkey)
502 if not c.signers_enum(0):
503 self.error = 'No key for signing [%s]' % sender
504 return False
505 c.set_passphrase_cb(lambda x, y, z: sign_passphrase)
506 try:
507
508 c.op_sign(plain, sig, mode.DETACH)
509 sig.seek(0, 0)
510
511 payload = MIMEMultipart.MIMEMultipart('signed',
512 boundary=None,
513 _subparts=None,
514 **dict(
515 micalg="pgp-sha1",
516 protocol="application/pgp-signature"))
517
518 payload.attach(payload_in)
519
520 p = MIMEBase.MIMEBase("application", 'pgp-signature')
521 p.set_payload(sig.read())
522 payload.attach(p)
523
524 payload_in = payload
525 except errors.GPGMEError, ex:
526 self.error = "GPG error: %s" % ex.getstring()
527 return False
528
529
530
531 if encrypt:
532 core.check_version(None)
533 plain = core.Data(payload_in.as_string())
534 cipher = core.Data()
535 c = core.Context()
536 c.set_armor(1)
537
538 recipients = []
539 rec = to[:]
540 if cc:
541 rec.extend(cc)
542 if bcc:
543 rec.extend(bcc)
544 for addr in rec:
545 c.op_keylist_start(addr, 0)
546 r = c.op_keylist_next()
547 if r is None:
548 self.error = 'No key for [%s]' % addr
549 return False
550 recipients.append(r)
551 try:
552
553 c.op_encrypt(recipients, 1, plain, cipher)
554 cipher.seek(0, 0)
555
556 payload = MIMEMultipart.MIMEMultipart('encrypted',
557 boundary=None,
558 _subparts=None,
559 **dict(protocol="application/pgp-encrypted"))
560 p = MIMEBase.MIMEBase("application", 'pgp-encrypted')
561 p.set_payload("Version: 1\r\n")
562 payload.attach(p)
563 p = MIMEBase.MIMEBase("application", 'octet-stream')
564 p.set_payload(cipher.read())
565 payload.attach(p)
566 except errors.GPGMEError, ex:
567 self.error = "GPG error: %s" % ex.getstring()
568 return False
569
570
571
572 elif cipher_type == 'x509':
573 if not sign and not encrypt:
574 self.error = "No sign and no encrypt is set but cipher type to x509"
575 return False
576 x509_sign_keyfile = self.settings.x509_sign_keyfile
577 if self.settings.x509_sign_certfile:
578 x509_sign_certfile = self.settings.x509_sign_certfile
579 else:
580
581
582 x509_sign_certfile = self.settings.x509_sign_keyfile
583
584 x509_crypt_certfiles = self.settings.x509_crypt_certfiles
585 x509_nocerts = self.settings.x509_nocerts
586
587
588 try:
589 from M2Crypto import BIO, SMIME, X509
590 except Exception, e:
591 self.error = "Can't load M2Crypto module"
592 return False
593 msg_bio = BIO.MemoryBuffer(payload_in.as_string())
594 s = SMIME.SMIME()
595
596
597 if sign:
598
599 try:
600 s.load_key(x509_sign_keyfile, x509_sign_certfile,
601 callback=lambda x: sign_passphrase)
602 except Exception, e:
603 self.error = "Something went wrong on certificate / private key loading: <%s>" % str(e)
604 return False
605 try:
606 if x509_nocerts:
607 flags = SMIME.PKCS7_NOCERTS
608 else:
609 flags = 0
610 if not encrypt:
611 flags += SMIME.PKCS7_DETACHED
612 p7 = s.sign(msg_bio, flags=flags)
613 msg_bio = BIO.MemoryBuffer(payload_in.as_string(
614 ))
615 except Exception, e:
616 self.error = "Something went wrong on signing: <%s> %s" % (
617 str(e), str(flags))
618 return False
619
620
621 if encrypt:
622 try:
623 sk = X509.X509_Stack()
624 if not isinstance(x509_crypt_certfiles, (list, tuple)):
625 x509_crypt_certfiles = [x509_crypt_certfiles]
626
627
628 for x in x509_crypt_certfiles:
629 sk.push(X509.load_cert(x))
630 s.set_x509_stack(sk)
631
632 s.set_cipher(SMIME.Cipher('des_ede3_cbc'))
633 tmp_bio = BIO.MemoryBuffer()
634 if sign:
635 s.write(tmp_bio, p7)
636 else:
637 tmp_bio.write(payload_in.as_string())
638 p7 = s.encrypt(tmp_bio)
639 except Exception, e:
640 self.error = "Something went wrong on encrypting: <%s>" % str(e)
641 return False
642
643
644 out = BIO.MemoryBuffer()
645 if encrypt:
646 s.write(out, p7)
647 else:
648 if sign:
649 s.write(out, p7, msg_bio, SMIME.PKCS7_DETACHED)
650 else:
651 out.write('\r\n')
652 out.write(payload_in.as_string())
653 out.close()
654 st = str(out.read())
655 payload = message_from_string(st)
656 else:
657
658 payload = payload_in
659
660 if from_address:
661 payload['From'] = encoded_or_raw(from_address.decode(encoding))
662 else:
663 payload['From'] = encoded_or_raw(sender.decode(encoding))
664 origTo = to[:]
665 if to:
666 payload['To'] = encoded_or_raw(', '.join(to).decode(encoding))
667 if reply_to:
668 payload['Reply-To'] = encoded_or_raw(reply_to.decode(encoding))
669 if cc:
670 payload['Cc'] = encoded_or_raw(', '.join(cc).decode(encoding))
671 to.extend(cc)
672 if bcc:
673 to.extend(bcc)
674 payload['Subject'] = encoded_or_raw(subject.decode(encoding))
675 payload['Date'] = email.utils.formatdate()
676 for k, v in headers.iteritems():
677 payload[k] = encoded_or_raw(v.decode(encoding))
678 result = {}
679 try:
680 if self.settings.server == 'logging':
681 logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' %
682 ('-' * 40, sender,
683 ', '.join(to), subject,
684 text or html, '-' * 40))
685 elif self.settings.server == 'gae':
686 xcc = dict()
687 if cc:
688 xcc['cc'] = cc
689 if bcc:
690 xcc['bcc'] = bcc
691 if reply_to:
692 xcc['reply_to'] = reply_to
693 from google.appengine.api import mail
694 attachments = attachments and [(a.my_filename, a.my_payload) for a in attachments if not raw]
695 if attachments:
696 result = mail.send_mail(
697 sender=sender, to=origTo,
698 subject=subject, body=text, html=html,
699 attachments=attachments, **xcc)
700 elif html and (not raw):
701 result = mail.send_mail(
702 sender=sender, to=origTo,
703 subject=subject, body=text, html=html, **xcc)
704 else:
705 result = mail.send_mail(
706 sender=sender, to=origTo,
707 subject=subject, body=text, **xcc)
708 else:
709 smtp_args = self.settings.server.split(':')
710 if self.settings.ssl:
711 server = smtplib.SMTP_SSL(*smtp_args)
712 else:
713 server = smtplib.SMTP(*smtp_args)
714 if self.settings.tls and not self.settings.ssl:
715 server.ehlo(self.settings.hostname)
716 server.starttls()
717 server.ehlo(self.settings.hostname)
718 if self.settings.login:
719 server.login(*self.settings.login.split(':', 1))
720 result = server.sendmail(
721 sender, to, payload.as_string())
722 server.quit()
723 except Exception, e:
724 logger.warn('Mail.send failure:%s' % e)
725 self.result = result
726 self.error = e
727 return False
728 self.result = result
729 self.error = None
730 return True
731
734
735 """
736 Usage:
737
738 form = FORM(Recaptcha(public_key='...',private_key='...'))
739
740 or
741
742 form = SQLFORM(...)
743 form.append(Recaptcha(public_key='...',private_key='...'))
744 """
745
746 API_SSL_SERVER = 'https://www.google.com/recaptcha/api'
747 API_SERVER = 'http://www.google.com/recaptcha/api'
748 VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify'
749
750 - def __init__(
751 self,
752 request=None,
753 public_key='',
754 private_key='',
755 use_ssl=False,
756 error=None,
757 error_message='invalid',
758 label='Verify:',
759 options='',
760 comment = '',
761 ajax=False
762 ):
763 self.request_vars = request and request.vars or current.request.vars
764 self.remote_addr = request.env.remote_addr
765 self.public_key = public_key
766 self.private_key = private_key
767 self.use_ssl = use_ssl
768 self.error = error
769 self.errors = Storage()
770 self.error_message = error_message
771 self.components = []
772 self.attributes = {}
773 self.label = label
774 self.options = options
775 self.comment = comment
776 self.ajax = ajax
777
779
780
781
782 recaptcha_challenge_field = \
783 self.request_vars.recaptcha_challenge_field
784 recaptcha_response_field = \
785 self.request_vars.recaptcha_response_field
786 private_key = self.private_key
787 remoteip = self.remote_addr
788 if not (recaptcha_response_field and recaptcha_challenge_field
789 and len(recaptcha_response_field)
790 and len(recaptcha_challenge_field)):
791 self.errors['captcha'] = self.error_message
792 return False
793 params = urllib.urlencode({
794 'privatekey': private_key,
795 'remoteip': remoteip,
796 'challenge': recaptcha_challenge_field,
797 'response': recaptcha_response_field,
798 })
799 request = urllib2.Request(
800 url=self.VERIFY_SERVER,
801 data=params,
802 headers={'Content-type': 'application/x-www-form-urlencoded',
803 'User-agent': 'reCAPTCHA Python'})
804 httpresp = urllib2.urlopen(request)
805 return_values = httpresp.read().splitlines()
806 httpresp.close()
807 return_code = return_values[0]
808 if return_code == 'true':
809 del self.request_vars.recaptcha_challenge_field
810 del self.request_vars.recaptcha_response_field
811 self.request_vars.captcha = ''
812 return True
813 else:
814
815
816 self.error = return_values[1]
817 self.errors['captcha'] = self.error_message
818 return False
819
821 public_key = self.public_key
822 use_ssl = self.use_ssl
823 error_param = ''
824 if self.error:
825 error_param = '&error=%s' % self.error
826 if use_ssl:
827 server = self.API_SSL_SERVER
828 else:
829 server = self.API_SERVER
830 if not self.ajax:
831 captcha = DIV(
832 SCRIPT("var RecaptchaOptions = {%s};" % self.options),
833 SCRIPT(_type="text/javascript",
834 _src="%s/challenge?k=%s%s" % (server, public_key, error_param)),
835 TAG.noscript(
836 IFRAME(
837 _src="%s/noscript?k=%s%s" % (
838 server, public_key, error_param),
839 _height="300", _width="500", _frameborder="0"), BR(),
840 INPUT(
841 _type='hidden', _name='recaptcha_response_field',
842 _value='manual_challenge')), _id='recaptcha')
843
844 else:
845
846 url_recaptcha_js = "%s/js/recaptcha_ajax.js" % server
847 RecaptchaOptions = "var RecaptchaOptions = {%s}" % self.options
848 script = """%(options)s;
849 jQuery.getScript('%(url)s',function() {
850 Recaptcha.create('%(public_key)s',
851 'recaptcha',jQuery.extend(RecaptchaOptions,{'callback':Recaptcha.focus_response_field}))
852 }) """ % ({'options':RecaptchaOptions,'url':url_recaptcha_js,'public_key':public_key})
853 captcha = DIV(
854 SCRIPT(
855 script,
856 _type="text/javascript",
857 ),
858 TAG.noscript(
859 IFRAME(
860 _src="%s/noscript?k=%s%s" % (
861 server, public_key, error_param),
862 _height="300", _width="500", _frameborder="0"), BR(),
863 INPUT(
864 _type='hidden', _name='recaptcha_response_field',
865 _value='manual_challenge')), _id='recaptcha')
866
867 if not self.errors.captcha:
868 return XML(captcha).xml()
869 else:
870 captcha.append(DIV(self.errors['captcha'], _class='error'))
871 return XML(captcha).xml()
872
873
874 -def addrow(form, a, b, c, style, _id, position=-1):
875 if style == "divs":
876 form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'),
877 DIV(b, _class='w2p_fw'),
878 DIV(c, _class='w2p_fc'),
879 _id=_id))
880 elif style == "table2cols":
881 form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
882 TD(c, _class='w2p_fc')))
883 form[0].insert(position + 1, TR(TD(b, _class='w2p_fw'),
884 _colspan=2, _id=_id))
885 elif style == "ul":
886 form[0].insert(position, LI(DIV(LABEL(a), _class='w2p_fl'),
887 DIV(b, _class='w2p_fw'),
888 DIV(c, _class='w2p_fc'),
889 _id=_id))
890 elif style == "bootstrap":
891 form[0].insert(position, DIV(LABEL(a, _class='control-label'),
892 DIV(b, SPAN(c, _class='inline-help'),
893 _class='controls'),
894 _class='control-group', _id=_id))
895 else:
896 form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
897 TD(b, _class='w2p_fw'),
898 TD(c, _class='w2p_fc'), _id=_id))
899
900
901 -class Auth(object):
902
903 default_settings = dict(
904 hideerror=False,
905 password_min_length=4,
906 cas_maps=None,
907 reset_password_requires_verification=False,
908 registration_requires_verification=False,
909 registration_requires_approval=False,
910 login_after_registration=False,
911 login_after_password_change=True,
912 alternate_requires_registration=False,
913 create_user_groups="user_%(id)s",
914 everybody_group_id=None,
915 manager_actions={},
916 auth_manager_role=None,
917 login_captcha=None,
918 register_captcha=None,
919 pre_registration_div=None,
920 retrieve_username_captcha=None,
921 retrieve_password_captcha=None,
922 captcha=None,
923 expiration=3600,
924 long_expiration=3600 * 30 * 24,
925 remember_me_form=True,
926 allow_basic_login=False,
927 allow_basic_login_only=False,
928 on_failed_authentication=lambda x: redirect(x),
929 formstyle="table3cols",
930 label_separator=": ",
931 logging_enabled = True,
932 allow_delete_accounts=False,
933 password_field='password',
934 table_user_name='auth_user',
935 table_group_name='auth_group',
936 table_membership_name='auth_membership',
937 table_permission_name='auth_permission',
938 table_event_name='auth_event',
939 table_cas_name='auth_cas',
940 table_user=None,
941 table_group=None,
942 table_membership=None,
943 table_permission=None,
944 table_event=None,
945 table_cas=None,
946 showid=False,
947 use_username=False,
948 login_email_validate=True,
949 login_userfield=None,
950 multi_login=False,
951 logout_onlogout=None,
952 register_fields=None,
953 register_verify_password=True,
954 profile_fields=None,
955 email_case_sensitive=True,
956 username_case_sensitive=True,
957 update_fields = ['email'],
958 ondelete="CASCADE",
959 client_side = True,
960 renew_session_onlogin=True,
961 renew_session_onlogout=True,
962 keep_session_onlogin=True,
963 keep_session_onlogout=False,
964 wiki = Settings(),
965 )
966
967 default_messages = dict(
968 login_button='Login',
969 register_button='Register',
970 password_reset_button='Request reset password',
971 password_change_button='Change password',
972 profile_save_button='Apply changes',
973 submit_button='Submit',
974 verify_password='Verify Password',
975 delete_label='Check to delete',
976 function_disabled='Function disabled',
977 access_denied='Insufficient privileges',
978 registration_verifying='Registration needs verification',
979 registration_pending='Registration is pending approval',
980 email_taken='This email already has an account',
981 invalid_username='Invalid username',
982 username_taken='Username already taken',
983 login_disabled='Login disabled by administrator',
984 logged_in='Logged in',
985 email_sent='Email sent',
986 unable_to_send_email='Unable to send email',
987 email_verified='Email verified',
988 logged_out='Logged out',
989 registration_successful='Registration successful',
990 invalid_email='Invalid email',
991 unable_send_email='Unable to send email',
992 invalid_login='Invalid login',
993 invalid_user='Invalid user',
994 invalid_password='Invalid password',
995 is_empty="Cannot be empty",
996 mismatched_password="Password fields don't match",
997 verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email',
998 verify_email_subject='Email verification',
999 username_sent='Your username was emailed to you',
1000 new_password_sent='A new password was emailed to you',
1001 password_changed='Password changed',
1002 retrieve_username='Your username is: %(username)s',
1003 retrieve_username_subject='Username retrieve',
1004 retrieve_password='Your password is: %(password)s',
1005 retrieve_password_subject='Password retrieve',
1006 reset_password=
1007 'Click on the link %(link)s to reset your password',
1008 reset_password_subject='Password reset',
1009 invalid_reset_password='Invalid reset password',
1010 profile_updated='Profile updated',
1011 new_password='New password',
1012 old_password='Old password',
1013 group_description='Group uniquely assigned to user %(id)s',
1014 register_log='User %(id)s Registered',
1015 login_log='User %(id)s Logged-in',
1016 login_failed_log=None,
1017 logout_log='User %(id)s Logged-out',
1018 profile_log='User %(id)s Profile updated',
1019 verify_email_log='User %(id)s Verification email sent',
1020 retrieve_username_log='User %(id)s Username retrieved',
1021 retrieve_password_log='User %(id)s Password retrieved',
1022 reset_password_log='User %(id)s Password reset',
1023 change_password_log='User %(id)s Password changed',
1024 add_group_log='Group %(group_id)s created',
1025 del_group_log='Group %(group_id)s deleted',
1026 add_membership_log=None,
1027 del_membership_log=None,
1028 has_membership_log=None,
1029 add_permission_log=None,
1030 del_permission_log=None,
1031 has_permission_log=None,
1032 impersonate_log='User %(id)s is impersonating %(other_id)s',
1033 label_first_name='First name',
1034 label_last_name='Last name',
1035 label_username='Username',
1036 label_email='E-mail',
1037 label_password='Password',
1038 label_registration_key='Registration key',
1039 label_reset_password_key='Reset Password key',
1040 label_registration_id='Registration identifier',
1041 label_role='Role',
1042 label_description='Description',
1043 label_user_id='User ID',
1044 label_group_id='Group ID',
1045 label_name='Name',
1046 label_table_name='Object or table name',
1047 label_record_id='Record ID',
1048 label_time_stamp='Timestamp',
1049 label_client_ip='Client IP',
1050 label_origin='Origin',
1051 label_remember_me="Remember me (for 30 days)",
1052 verify_password_comment='please input your password again',
1053 )
1054
1055 """
1056 Class for authentication, authorization, role based access control.
1057
1058 Includes:
1059
1060 - registration and profile
1061 - login and logout
1062 - username and password retrieval
1063 - event logging
1064 - role creation and assignment
1065 - user defined group/role based permission
1066
1067 Authentication Example:
1068
1069 from gluon.contrib.utils import *
1070 mail=Mail()
1071 mail.settings.server='smtp.gmail.com:587'
1072 mail.settings.sender='you@somewhere.com'
1073 mail.settings.login='username:password'
1074 auth=Auth(db)
1075 auth.settings.mailer=mail
1076 # auth.settings....=...
1077 auth.define_tables()
1078 def authentication():
1079 return dict(form=auth())
1080
1081 exposes:
1082
1083 - http://.../{application}/{controller}/authentication/login
1084 - http://.../{application}/{controller}/authentication/logout
1085 - http://.../{application}/{controller}/authentication/register
1086 - http://.../{application}/{controller}/authentication/verify_email
1087 - http://.../{application}/{controller}/authentication/retrieve_username
1088 - http://.../{application}/{controller}/authentication/retrieve_password
1089 - http://.../{application}/{controller}/authentication/reset_password
1090 - http://.../{application}/{controller}/authentication/profile
1091 - http://.../{application}/{controller}/authentication/change_password
1092
1093 On registration a group with role=new_user.id is created
1094 and user is given membership of this group.
1095
1096 You can create a group with:
1097
1098 group_id=auth.add_group('Manager', 'can access the manage action')
1099 auth.add_permission(group_id, 'access to manage')
1100
1101 Here \"access to manage\" is just a user defined string.
1102 You can give access to a user:
1103
1104 auth.add_membership(group_id, user_id)
1105
1106 If user id is omitted, the logged in user is assumed
1107
1108 Then you can decorate any action:
1109
1110 @auth.requires_permission('access to manage')
1111 def manage():
1112 return dict()
1113
1114 You can restrict a permission to a specific table:
1115
1116 auth.add_permission(group_id, 'edit', db.sometable)
1117 @auth.requires_permission('edit', db.sometable)
1118
1119 Or to a specific record:
1120
1121 auth.add_permission(group_id, 'edit', db.sometable, 45)
1122 @auth.requires_permission('edit', db.sometable, 45)
1123
1124 If authorization is not granted calls:
1125
1126 auth.settings.on_failed_authorization
1127
1128 Other options:
1129
1130 auth.settings.mailer=None
1131 auth.settings.expiration=3600 # seconds
1132
1133 ...
1134
1135 ### these are messages that can be customized
1136 ...
1137 """
1138
1139 @staticmethod
1150
1151 - def url(self, f=None, args=None, vars=None, scheme=False):
1152 if args is None:
1153 args = []
1154 if vars is None:
1155 vars = {}
1156 return URL(c=self.settings.controller,
1157 f=f, args=args, vars=vars, scheme=scheme)
1158
1161
1162 - def __init__(self, environment=None, db=None, mailer=True,
1163 hmac_key=None, controller='default', function='user',
1164 cas_provider=None, signature=True, secure=False,
1165 csrf_prevention=True, propagate_extension=None):
1166 """
1167 auth=Auth(db)
1168
1169 - environment is there for legacy but unused (awful)
1170 - db has to be the database where to create tables for authentication
1171 - mailer=Mail(...) or None (no mailed) or True (make a mailer)
1172 - hmac_key can be a hmac_key or hmac_key=Auth.get_or_create_key()
1173 - controller (where is the user action?)
1174 - cas_provider (delegate authentication to the URL, CAS2)
1175 """
1176
1177 if not db and environment and isinstance(environment, DAL):
1178 db = environment
1179 self.db = db
1180 self.environment = current
1181 self.csrf_prevention = csrf_prevention
1182 request = current.request
1183 session = current.session
1184 auth = session.auth
1185 self.user_groups = auth and auth.user_groups or {}
1186 if secure:
1187 request.requires_https()
1188 now = request.now
1189
1190
1191
1192
1193 if auth:
1194 delta = datetime.timedelta(days=0, seconds=auth.expiration)
1195 if auth.last_visit and auth.last_visit + delta > now:
1196 self.user = auth.user
1197
1198 if (now - auth.last_visit).seconds > (auth.expiration / 10):
1199 auth.last_visit = request.now
1200 else:
1201 self.user = None
1202 if session.auth:
1203 del session.auth
1204 session.renew(clear_session=True)
1205 else:
1206 self.user = None
1207 if session.auth:
1208 del session.auth
1209
1210
1211 url_index = URL(controller, 'index')
1212 url_login = URL(controller, function, args='login',
1213 extension = propagate_extension)
1214
1215
1216 settings = self.settings = Settings()
1217 settings.update(Auth.default_settings)
1218 settings.update(
1219 cas_domains=[request.env.http_host],
1220 cas_provider=cas_provider,
1221 cas_actions=dict(login='login',
1222 validate='validate',
1223 servicevalidate='serviceValidate',
1224 proxyvalidate='proxyValidate',
1225 logout='logout'),
1226 extra_fields={},
1227 actions_disabled=[],
1228 controller=controller,
1229 function=function,
1230 login_url=url_login,
1231 logged_url=URL(controller, function, args='profile'),
1232 download_url=URL(controller, 'download'),
1233 mailer=(mailer == True) and Mail() or mailer,
1234 on_failed_authorization =
1235 URL(controller, function, args='not_authorized'),
1236 login_next = url_index,
1237 login_onvalidation = [],
1238 login_onaccept = [],
1239 login_onfail = [],
1240 login_methods = [self],
1241 login_form = self,
1242 logout_next = url_index,
1243 logout_onlogout = None,
1244 register_next = url_index,
1245 register_onvalidation = [],
1246 register_onaccept = [],
1247 verify_email_next = url_login,
1248 verify_email_onaccept = [],
1249 profile_next = url_index,
1250 profile_onvalidation = [],
1251 profile_onaccept = [],
1252 retrieve_username_next = url_index,
1253 retrieve_password_next = url_index,
1254 request_reset_password_next = url_login,
1255 reset_password_next = url_index,
1256 change_password_next = url_index,
1257 change_password_onvalidation = [],
1258 change_password_onaccept = [],
1259 retrieve_password_onvalidation = [],
1260 reset_password_onvalidation = [],
1261 reset_password_onaccept = [],
1262 hmac_key = hmac_key,
1263 )
1264 settings.lock_keys = True
1265
1266
1267 messages = self.messages = Messages(current.T)
1268 messages.update(Auth.default_messages)
1269 messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'),
1270 'Please ',
1271 A('login',
1272 _href=self.settings.login_url +
1273 ('?_next=' + urllib.quote(current.request.env.http_web2py_component_location))
1274 if current.request.env.http_web2py_component_location else ''),
1275 ' to view this content.',
1276 _class='not-authorized alert alert-block'))
1277 messages.lock_keys = True
1278
1279
1280 response = current.response
1281 if auth and auth.remember:
1282
1283 response.session_cookie_expires = auth.expiration
1284 if signature:
1285 self.define_signature()
1286 else:
1287 self.signature = None
1288
1294
1296 "accessor for auth.user_id"
1297 return self.user and self.user.id or None
1298
1299 user_id = property(_get_user_id, doc="user.id or None")
1300
1302 return self.db[self.settings.table_user_name]
1303
1305 return self.db[self.settings.table_group_name]
1306
1308 return self.db[self.settings.table_membership_name]
1309
1311 return self.db[self.settings.table_permission_name]
1312
1314 return self.db[self.settings.table_event_name]
1315
1318
1319 - def _HTTP(self, *a, **b):
1320 """
1321 only used in lambda: self._HTTP(404)
1322 """
1323
1324 raise HTTP(*a, **b)
1325
1327 """
1328 usage:
1329
1330 def authentication(): return dict(form=auth())
1331 """
1332
1333 request = current.request
1334 args = request.args
1335 if not args:
1336 redirect(self.url(args='login', vars=request.vars))
1337 elif args[0] in self.settings.actions_disabled:
1338 raise HTTP(404)
1339 if args[0] in ('login', 'logout', 'register', 'verify_email',
1340 'retrieve_username', 'retrieve_password',
1341 'reset_password', 'request_reset_password',
1342 'change_password', 'profile', 'groups',
1343 'impersonate', 'not_authorized'):
1344 if len(request.args) >= 2 and args[0] == 'impersonate':
1345 return getattr(self, args[0])(request.args[1])
1346 else:
1347 return getattr(self, args[0])()
1348 elif args[0] == 'cas' and not self.settings.cas_provider:
1349 if args(1) == self.settings.cas_actions['login']:
1350 return self.cas_login(version=2)
1351 elif args(1) == self.settings.cas_actions['validate']:
1352 return self.cas_validate(version=1)
1353 elif args(1) == self.settings.cas_actions['servicevalidate']:
1354 return self.cas_validate(version=2, proxy=False)
1355 elif args(1) == self.settings.cas_actions['proxyvalidate']:
1356 return self.cas_validate(version=2, proxy=True)
1357 elif args(1) == self.settings.cas_actions['logout']:
1358 return self.logout(next=request.vars.service or DEFAULT)
1359 else:
1360 raise HTTP(404)
1361
1362 - def navbar(self, prefix='Welcome', action=None,
1363 separators=(' [ ', ' | ', ' ] '), user_identifier=DEFAULT,
1364 referrer_actions=DEFAULT, mode='default'):
1365 """ Navbar with support for more templates
1366 This uses some code from the old navbar.
1367
1368 Keyword arguments:
1369 mode -- see options for list of
1370
1371 """
1372 items = []
1373 self.bar = ''
1374 T = current.T
1375 referrer_actions = [] if not referrer_actions else referrer_actions
1376 if not action:
1377 action = self.url(self.settings.function)
1378
1379 request = current.request
1380 if URL() == action:
1381 next = ''
1382 else:
1383 next = '?_next=' + urllib.quote(URL(args=request.args,
1384 vars=request.get_vars))
1385 href = lambda function: '%s/%s%s' % (action, function, next
1386 if referrer_actions is DEFAULT
1387 or function in referrer_actions
1388 else '')
1389 if isinstance(prefix, str):
1390 prefix = T(prefix)
1391 if prefix:
1392 prefix = prefix.strip() + ' '
1393
1394 def Anr(*a, **b):
1395 b['_rel'] = 'nofollow'
1396 return A(*a, **b)
1397
1398 if self.user_id:
1399 logout_next = self.settings.logout_next
1400 items.append({'name': T('Logout'),
1401 'href': '%s/logout?_next=%s' % (action,
1402 urllib.quote(
1403 logout_next)),
1404 'icon': 'icon-off'})
1405 if not 'profile' in self.settings.actions_disabled:
1406 items.append({'name': T('Profile'), 'href': href('profile'),
1407 'icon': 'icon-user'})
1408 if not 'change_password' in self.settings.actions_disabled:
1409 items.append({'name': T('Password'),
1410 'href': href('change_password'),
1411 'icon': 'icon-lock'})
1412
1413 if user_identifier is DEFAULT:
1414 user_identifier = '%(first_name)s'
1415 if callable(user_identifier):
1416 user_identifier = user_identifier(self.user)
1417 elif ((isinstance(user_identifier, str) or
1418 type(user_identifier).__name__ == 'lazyT') and
1419 re.search(r'%\(.+\)s', user_identifier)):
1420 user_identifier = user_identifier % self.user
1421 if not user_identifier:
1422 user_identifier = ''
1423 else:
1424 items.append({'name': T('Login'), 'href': href('login'),
1425 'icon': 'icon-off'})
1426 if not 'register' in self.settings.actions_disabled:
1427 items.append({'name': T('Register'), 'href': href('register'),
1428 'icon': 'icon-user'})
1429 if not 'request_reset_password' in self.settings.actions_disabled:
1430 items.append({'name': T('Lost password?'),
1431 'href': href('request_reset_password'),
1432 'icon': 'icon-lock'})
1433 if (self.settings.use_username and not
1434 'retrieve_username' in self.settings.actions_disabled):
1435 items.append({'name': T('Forgot username?'),
1436 'href': href('retrieve_username'),
1437 'icon': 'icon-edit'})
1438
1439 def menu():
1440 self.bar = [(items[0]['name'], False, items[0]['href'], [])]
1441 del items[0]
1442 for item in items:
1443 self.bar[0][3].append((item['name'], False, item['href']))
1444
1445 def bootstrap3():
1446 def rename(icon): return icon+' '+icon.replace('icon','glyphicon')
1447 self.bar = UL(LI(Anr(I(_class=rename('icon '+items[0]['icon'])),
1448 ' ' + items[0]['name'],
1449 _href=items[0]['href'])),_class='dropdown-menu')
1450 del items[0]
1451 for item in items:
1452 self.bar.insert(-1, LI(Anr(I(_class=rename('icon '+item['icon'])),
1453 ' ' + item['name'],
1454 _href=item['href'])))
1455 self.bar.insert(-1, LI('', _class='divider'))
1456 if self.user_id:
1457 self.bar = LI(Anr(prefix, user_identifier, _href='#'),
1458 self.bar,_class='dropdown')
1459 else:
1460 self.bar = LI(Anr(T('Login'),
1461 _href='#',_class="dropdown-toggle",
1462 data={'toggle':'dropdown'}), self.bar,
1463 _class='dropdown')
1464
1465
1466
1467 def bare():
1468 """ In order to do advanced customization we only need the
1469 prefix, the user_identifier and the href attribute of items
1470
1471 Example:
1472
1473 # in module custom_layout.py
1474 from gluon import *
1475 def navbar(auth_navbar):
1476 bar = auth_navbar
1477 user = bar["user"]
1478
1479 if not user:
1480 btn_login = A(current.T("Login"),
1481 _href=bar["login"],
1482 _class="btn btn-success",
1483 _rel="nofollow")
1484 btn_register = A(current.T("Sign up"),
1485 _href=bar["register"],
1486 _class="btn btn-primary",
1487 _rel="nofollow")
1488 return DIV(btn_register, btn_login, _class="btn-group")
1489 else:
1490 toggletext = "%s back %s" % (bar["prefix"], user)
1491 toggle = A(toggletext,
1492 _href="#",
1493 _class="dropdown-toggle",
1494 _rel="nofollow",
1495 **{"_data-toggle": "dropdown"})
1496 li_profile = LI(A(I(_class="icon-user"), ' ',
1497 current.T("Account details"),
1498 _href=bar["profile"], _rel="nofollow"))
1499 li_custom = LI(A(I(_class="icon-book"), ' ',
1500 current.T("My Agenda"),
1501 _href="#", rel="nofollow"))
1502 li_logout = LI(A(I(_class="icon-off"), ' ',
1503 current.T("logout"),
1504 _href=bar["logout"], _rel="nofollow"))
1505 dropdown = UL(li_profile,
1506 li_custom,
1507 LI('', _class="divider"),
1508 li_logout,
1509 _class="dropdown-menu", _role="menu")
1510
1511 return LI(toggle, dropdown, _class="dropdown")
1512
1513 # in models db.py
1514 import custom_layout as custom
1515
1516 # in layout.html
1517 <ul id="navbar" class="nav pull-right">
1518 {{='auth' in globals() and \
1519 custom.navbar(auth.navbar(mode='bare')) or ''}}</ul>
1520
1521 """
1522 bare = {}
1523
1524 bare['prefix'] = prefix
1525 bare['user'] = user_identifier if self.user_id else None
1526
1527 for i in items:
1528 if i['name'] == T('Login'):
1529 k = 'login'
1530 elif i['name'] == T('Register'):
1531 k = 'register'
1532 elif i['name'] == T('Lost password?'):
1533 k = 'request_reset_password'
1534 elif i['name'] == T('Forgot username?'):
1535 k = 'retrieve_username'
1536 elif i['name'] == T('Logout'):
1537 k = 'logout'
1538 elif i['name'] == T('Profile'):
1539 k = 'profile'
1540 elif i['name'] == T('Password'):
1541 k = 'change_password'
1542
1543 bare[k] = i['href']
1544
1545 self.bar = bare
1546
1547 options = {'asmenu': menu,
1548 'dropdown': bootstrap3,
1549 'bare': bare
1550 }
1551
1552 if mode in options and callable(options[mode]):
1553 options[mode]()
1554 else:
1555 s1, s2, s3 = separators
1556 if self.user_id:
1557 self.bar = SPAN(prefix, user_identifier, s1,
1558 Anr(items[0]['name'],
1559 _href=items[0]['href']), s3,
1560 _class='auth_navbar')
1561 else:
1562 self.bar = SPAN(s1, Anr(items[0]['name'],
1563 _href=items[0]['href']), s3,
1564 _class='auth_navbar')
1565 for item in items[1:]:
1566 self.bar.insert(-1, s2)
1567 self.bar.insert(-1, Anr(item['name'], _href=item['href']))
1568
1569 return self.bar
1570
1572
1573 if type(migrate).__name__ == 'str':
1574 return (migrate + tablename + '.table')
1575 elif migrate == False:
1576 return False
1577 else:
1578 return True
1579
1580 - def enable_record_versioning(self,
1581 tables,
1582 archive_db=None,
1583 archive_names='%(tablename)s_archive',
1584 current_record='current_record',
1585 current_record_label=None):
1586 """
1587 to enable full record versioning (including auth tables):
1588
1589 auth = Auth(db)
1590 auth.define_tables(signature=True)
1591 # define our own tables
1592 db.define_table('mything',Field('name'),auth.signature)
1593 auth.enable_record_versioning(tables=db)
1594
1595 tables can be the db (all table) or a list of tables.
1596 only tables with modified_by and modified_on fiels (as created
1597 by auth.signature) will have versioning. Old record versions will be
1598 in table 'mything_archive' automatically defined.
1599
1600 when you enable enable_record_versioning, records are never
1601 deleted but marked with is_active=False.
1602
1603 enable_record_versioning enables a common_filter for
1604 every table that filters out records with is_active = False
1605
1606 Important: If you use auth.enable_record_versioning,
1607 do not use auth.archive or you will end up with duplicates.
1608 auth.archive does explicitly what enable_record_versioning
1609 does automatically.
1610
1611 """
1612 current_record_label = current_record_label or current.T(
1613 current_record.replace('_',' ').title())
1614 for table in tables:
1615 fieldnames = table.fields()
1616 if ('id' in fieldnames and
1617 'modified_on' in fieldnames and
1618 not current_record in fieldnames):
1619 table._enable_record_versioning(
1620 archive_db=archive_db,
1621 archive_name=archive_names,
1622 current_record=current_record,
1623 current_record_label=current_record_label)
1624
1634
1635 def represent(id, record=None, s=settings):
1636 try:
1637 user = s.table_user(id)
1638 return '%s %s' % (user.get("first_name", user.get("email")),
1639 user.get("last_name", ''))
1640 except:
1641 return id
1642 ondelete = self.settings.ondelete
1643 self.signature = db.Table(
1644 self.db, 'auth_signature',
1645 Field('is_active', 'boolean',
1646 default=True,
1647 readable=False, writable=False,
1648 label=T('Is Active')),
1649 Field('created_on', 'datetime',
1650 default=request.now,
1651 writable=False, readable=False,
1652 label=T('Created On')),
1653 Field('created_by',
1654 reference_user,
1655 default=lazy_user, represent=represent,
1656 writable=False, readable=False,
1657 label=T('Created By'), ondelete=ondelete),
1658 Field('modified_on', 'datetime',
1659 update=request.now, default=request.now,
1660 writable=False, readable=False,
1661 label=T('Modified On')),
1662 Field('modified_by',
1663 reference_user, represent=represent,
1664 default=lazy_user, update=lazy_user,
1665 writable=False, readable=False,
1666 label=T('Modified By'), ondelete=ondelete))
1667
1668 - def define_tables(self, username=None, signature=None,
1669 migrate=None, fake_migrate=None):
1670 """
1671 to be called unless tables are defined manually
1672
1673 usages:
1674
1675 # defines all needed tables and table files
1676 # 'myprefix_auth_user.table', ...
1677 auth.define_tables(migrate='myprefix_')
1678
1679 # defines all needed tables without migration/table files
1680 auth.define_tables(migrate=False)
1681
1682 """
1683
1684 db = self.db
1685 if migrate is None: migrate = db._migrate
1686 if fake_migrate is None: fake_migrate = db._fake_migrate
1687 settings = self.settings
1688 if username is None:
1689 username = settings.use_username
1690 else:
1691 settings.use_username = username
1692 if not self.signature:
1693 self.define_signature()
1694 if signature == True:
1695 signature_list = [self.signature]
1696 elif not signature:
1697 signature_list = []
1698 elif isinstance(signature, self.db.Table):
1699 signature_list = [signature]
1700 else:
1701 signature_list = signature
1702 is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty)
1703 is_crypted = CRYPT(key=settings.hmac_key,
1704 min_length=settings.password_min_length)
1705 is_unique_email = [
1706 IS_EMAIL(error_message=self.messages.invalid_email),
1707 IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name,
1708 error_message=self.messages.email_taken)]
1709 if not settings.email_case_sensitive:
1710 is_unique_email.insert(1, IS_LOWER())
1711 if not settings.table_user_name in db.tables:
1712 passfield = settings.password_field
1713 extra_fields = settings.extra_fields.get(
1714 settings.table_user_name, []) + signature_list
1715 if username or settings.cas_provider:
1716 is_unique_username = \
1717 [IS_MATCH('[\w\.\-]+', strict=True,
1718 error_message=self.messages.invalid_username),
1719 IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name,
1720 error_message=self.messages.username_taken)]
1721 if not settings.username_case_sensitive:
1722 is_unique_username.insert(1, IS_LOWER())
1723 db.define_table(
1724 settings.table_user_name,
1725 Field('first_name', length=128, default='',
1726 label=self.messages.label_first_name,
1727 requires=is_not_empty),
1728 Field('last_name', length=128, default='',
1729 label=self.messages.label_last_name,
1730 requires=is_not_empty),
1731 Field('email', length=512, default='',
1732 label=self.messages.label_email,
1733 requires=is_unique_email),
1734 Field('username', length=128, default='',
1735 label=self.messages.label_username,
1736 requires=is_unique_username),
1737 Field(passfield, 'password', length=512,
1738 readable=False, label=self.messages.label_password,
1739 requires=[is_crypted]),
1740 Field('registration_key', length=512,
1741 writable=False, readable=False, default='',
1742 label=self.messages.label_registration_key),
1743 Field('reset_password_key', length=512,
1744 writable=False, readable=False, default='',
1745 label=self.messages.label_reset_password_key),
1746 Field('registration_id', length=512,
1747 writable=False, readable=False, default='',
1748 label=self.messages.label_registration_id),
1749 *extra_fields,
1750 **dict(
1751 migrate=self.__get_migrate(settings.table_user_name,
1752 migrate),
1753 fake_migrate=fake_migrate,
1754 format='%(username)s'))
1755 else:
1756 db.define_table(
1757 settings.table_user_name,
1758 Field('first_name', length=128, default='',
1759 label=self.messages.label_first_name,
1760 requires=is_not_empty),
1761 Field('last_name', length=128, default='',
1762 label=self.messages.label_last_name,
1763 requires=is_not_empty),
1764 Field('email', length=512, default='',
1765 label=self.messages.label_email,
1766 requires=is_unique_email),
1767 Field(passfield, 'password', length=512,
1768 readable=False, label=self.messages.label_password,
1769 requires=[is_crypted]),
1770 Field('registration_key', length=512,
1771 writable=False, readable=False, default='',
1772 label=self.messages.label_registration_key),
1773 Field('reset_password_key', length=512,
1774 writable=False, readable=False, default='',
1775 label=self.messages.label_reset_password_key),
1776 Field('registration_id', length=512,
1777 writable=False, readable=False, default='',
1778 label=self.messages.label_registration_id),
1779 *extra_fields,
1780 **dict(
1781 migrate=self.__get_migrate(settings.table_user_name,
1782 migrate),
1783 fake_migrate=fake_migrate,
1784 format='%(first_name)s %(last_name)s (%(id)s)'))
1785 reference_table_user = 'reference %s' % settings.table_user_name
1786 if not settings.table_group_name in db.tables:
1787 extra_fields = settings.extra_fields.get(
1788 settings.table_group_name, []) + signature_list
1789 db.define_table(
1790 settings.table_group_name,
1791 Field('role', length=512, default='',
1792 label=self.messages.label_role,
1793 requires=IS_NOT_IN_DB(
1794 db, '%s.role' % settings.table_group_name)),
1795 Field('description', 'text',
1796 label=self.messages.label_description),
1797 *extra_fields,
1798 **dict(
1799 migrate=self.__get_migrate(
1800 settings.table_group_name, migrate),
1801 fake_migrate=fake_migrate,
1802 format='%(role)s (%(id)s)'))
1803 reference_table_group = 'reference %s' % settings.table_group_name
1804 if not settings.table_membership_name in db.tables:
1805 extra_fields = settings.extra_fields.get(
1806 settings.table_membership_name, []) + signature_list
1807 db.define_table(
1808 settings.table_membership_name,
1809 Field('user_id', reference_table_user,
1810 label=self.messages.label_user_id),
1811 Field('group_id', reference_table_group,
1812 label=self.messages.label_group_id),
1813 *extra_fields,
1814 **dict(
1815 migrate=self.__get_migrate(
1816 settings.table_membership_name, migrate),
1817 fake_migrate=fake_migrate))
1818 if not settings.table_permission_name in db.tables:
1819 extra_fields = settings.extra_fields.get(
1820 settings.table_permission_name, []) + signature_list
1821 db.define_table(
1822 settings.table_permission_name,
1823 Field('group_id', reference_table_group,
1824 label=self.messages.label_group_id),
1825 Field('name', default='default', length=512,
1826 label=self.messages.label_name,
1827 requires=is_not_empty),
1828 Field('table_name', length=512,
1829 label=self.messages.label_table_name),
1830 Field('record_id', 'integer', default=0,
1831 label=self.messages.label_record_id,
1832 requires=IS_INT_IN_RANGE(0, 10 ** 9)),
1833 *extra_fields,
1834 **dict(
1835 migrate=self.__get_migrate(
1836 settings.table_permission_name, migrate),
1837 fake_migrate=fake_migrate))
1838 if not settings.table_event_name in db.tables:
1839 db.define_table(
1840 settings.table_event_name,
1841 Field('time_stamp', 'datetime',
1842 default=current.request.now,
1843 label=self.messages.label_time_stamp),
1844 Field('client_ip',
1845 default=current.request.client,
1846 label=self.messages.label_client_ip),
1847 Field('user_id', reference_table_user, default=None,
1848 label=self.messages.label_user_id),
1849 Field('origin', default='auth', length=512,
1850 label=self.messages.label_origin,
1851 requires=is_not_empty),
1852 Field('description', 'text', default='',
1853 label=self.messages.label_description,
1854 requires=is_not_empty),
1855 *settings.extra_fields.get(settings.table_event_name, []),
1856 **dict(
1857 migrate=self.__get_migrate(
1858 settings.table_event_name, migrate),
1859 fake_migrate=fake_migrate))
1860 now = current.request.now
1861 if settings.cas_domains:
1862 if not settings.table_cas_name in db.tables:
1863 db.define_table(
1864 settings.table_cas_name,
1865 Field('user_id', reference_table_user, default=None,
1866 label=self.messages.label_user_id),
1867 Field('created_on', 'datetime', default=now),
1868 Field('service', requires=IS_URL()),
1869 Field('ticket'),
1870 Field('renew', 'boolean', default=False),
1871 *settings.extra_fields.get(settings.table_cas_name, []),
1872 **dict(
1873 migrate=self.__get_migrate(
1874 settings.table_cas_name, migrate),
1875 fake_migrate=fake_migrate))
1876 if not db._lazy_tables:
1877 settings.table_user = db[settings.table_user_name]
1878 settings.table_group = db[settings.table_group_name]
1879 settings.table_membership = db[settings.table_membership_name]
1880 settings.table_permission = db[settings.table_permission_name]
1881 settings.table_event = db[settings.table_event_name]
1882 if settings.cas_domains:
1883 settings.table_cas = db[settings.table_cas_name]
1884
1885 if settings.cas_provider:
1886 settings.actions_disabled = \
1887 ['profile', 'register', 'change_password',
1888 'request_reset_password', 'retrieve_username']
1889 from gluon.contrib.login_methods.cas_auth import CasAuth
1890 maps = settings.cas_maps
1891 if not maps:
1892 table_user = self.table_user()
1893 maps = dict((name, lambda v, n=name: v.get(n, None)) for name in
1894 table_user.fields if name != 'id'
1895 and table_user[name].readable)
1896 maps['registration_id'] = \
1897 lambda v, p=settings.cas_provider: '%s/%s' % (p, v['user'])
1898 actions = [settings.cas_actions['login'],
1899 settings.cas_actions['servicevalidate'],
1900 settings.cas_actions['logout']]
1901 settings.login_form = CasAuth(
1902 casversion=2,
1903 urlbase=settings.cas_provider,
1904 actions=actions,
1905 maps=maps)
1906 return self
1907
1908 - def log_event(self, description, vars=None, origin='auth'):
1909 """
1910 usage:
1911
1912 auth.log_event(description='this happened', origin='auth')
1913 """
1914 if not self.settings.logging_enabled or not description:
1915 return
1916 elif self.is_logged_in():
1917 user_id = self.user.id
1918 else:
1919 user_id = None
1920 vars = vars or {}
1921
1922 if type(description).__name__ == 'lazyT':
1923 description = description.m
1924 self.table_event().insert(
1925 description=str(description % vars),
1926 origin=origin, user_id=user_id)
1927
1930 """
1931 Used for alternate login methods:
1932 If the user exists already then password is updated.
1933 If the user doesn't yet exist, then they are created.
1934 """
1935 table_user = self.table_user()
1936 user = None
1937 checks = []
1938
1939 for fieldname in ['registration_id', 'username', 'email']:
1940 if fieldname in table_user.fields() and \
1941 keys.get(fieldname, None):
1942 checks.append(fieldname)
1943 value = keys[fieldname]
1944 user = table_user(**{fieldname: value})
1945 if user:
1946 break
1947 if not checks:
1948 return None
1949 if not 'registration_id' in keys:
1950 keys['registration_id'] = keys[checks[0]]
1951
1952
1953 if 'registration_id' in checks \
1954 and user \
1955 and user.registration_id \
1956 and ('registration_id' not in keys or user.registration_id != str(keys['registration_id'])):
1957 user = None
1958 if user:
1959 if not get:
1960
1961 return None
1962 update_keys = dict(registration_id=keys['registration_id'])
1963 for key in update_fields:
1964 if key in keys:
1965 update_keys[key] = keys[key]
1966 user.update_record(**update_keys)
1967 elif checks:
1968 if not 'first_name' in keys and 'first_name' in table_user.fields:
1969 guess = keys.get('email', 'anonymous').split('@')[0]
1970 keys['first_name'] = keys.get('username', guess)
1971 user_id = table_user.insert(**table_user._filter_fields(keys))
1972 user = table_user[user_id]
1973 if self.settings.create_user_groups:
1974 group_id = self.add_group(
1975 self.settings.create_user_groups % user)
1976 self.add_membership(group_id, user_id)
1977 if self.settings.everybody_group_id:
1978 self.add_membership(self.settings.everybody_group_id, user_id)
1979 if login:
1980 self.user = user
1981 return user
1982
1983 - def basic(self, basic_auth_realm=False):
1984 """
1985 perform basic login.
1986
1987 :param basic_auth_realm: optional basic http authentication realm.
1988 :type basic_auth_realm: str or unicode or function or callable or boolean.
1989
1990 reads current.request.env.http_authorization
1991 and returns basic_allowed,basic_accepted,user.
1992
1993 if basic_auth_realm is defined is a callable it's return value
1994 is used to set the basic authentication realm, if it's a string
1995 its content is used instead. Otherwise basic authentication realm
1996 is set to the application name.
1997 If basic_auth_realm is None or False (the default) the behavior
1998 is to skip sending any challenge.
1999
2000 """
2001 if not self.settings.allow_basic_login:
2002 return (False, False, False)
2003 basic = current.request.env.http_authorization
2004 if basic_auth_realm:
2005 if callable(basic_auth_realm):
2006 basic_auth_realm = basic_auth_realm()
2007 elif isinstance(basic_auth_realm, (unicode, str)):
2008 basic_realm = unicode(basic_auth_realm)
2009 elif basic_auth_realm is True:
2010 basic_realm = u'' + current.request.application
2011 http_401 = HTTP(401, u'Not Authorized',
2012 **{'WWW-Authenticate': u'Basic realm="' + basic_realm + '"'})
2013 if not basic or not basic[:6].lower() == 'basic ':
2014 if basic_auth_realm:
2015 raise http_401
2016 return (True, False, False)
2017 (username, sep, password) = base64.b64decode(basic[6:]).partition(':')
2018 is_valid_user = sep and self.login_bare(username, password)
2019 if not is_valid_user and basic_auth_realm:
2020 raise http_401
2021 return (True, True, is_valid_user)
2022
2045
2054
2056 """
2057 logins user as specified by username (or email) and password
2058 """
2059 settings = self._get_login_settings()
2060 user = settings.table_user(**{settings.userfield: \
2061 username})
2062 if user and user.get(settings.passfield, False):
2063 password = settings.table_user[
2064 settings.passfield].validate(password)[0]
2065 if ((user.registration_key is None or
2066 not user.registration_key.strip()) and
2067 password == user[settings.passfield]):
2068 self.login_user(user)
2069 return user
2070 else:
2071
2072 for login_method in self.settings.login_methods:
2073 if login_method != self and \
2074 login_method(username, password):
2075 self.user = username
2076 return username
2077 return False
2078
2102
2103
2142 if self.is_logged_in() and not 'renew' in request.vars:
2143 return allow_access()
2144 elif not self.is_logged_in() and 'gateway' in request.vars:
2145 redirect(service)
2146
2147 def cas_onaccept(form, onaccept=onaccept):
2148 if not onaccept is DEFAULT:
2149 onaccept(form)
2150 return allow_access(interactivelogin=True)
2151 return self.login(next, onvalidation, cas_onaccept, log)
2152
2154 request = current.request
2155 db, table = self.db, self.table_cas()
2156 current.response.headers['Content-Type'] = 'text'
2157 ticket = request.vars.ticket
2158 renew = 'renew' in request.vars
2159 row = table(ticket=ticket)
2160 success = False
2161 if row:
2162 userfield = self.settings.login_userfield or 'username' \
2163 if 'username' in table.fields else 'email'
2164
2165 if ticket[0:3] == 'ST-' and \
2166 not ((row.renew and renew) ^ renew):
2167 user = self.table_user()(row.user_id)
2168 row.delete_record()
2169 success = True
2170
2171 def build_response(body):
2172 return '<?xml version="1.0" encoding="UTF-8"?>\n' +\
2173 TAG['cas:serviceResponse'](
2174 body, **{'_xmlns:cas': 'http://www.yale.edu/tp/cas'}).xml()
2175 if success:
2176 if version == 1:
2177 message = 'yes\n%s' % user[userfield]
2178 else:
2179 username = user.get('username', user[userfield])
2180 message = build_response(
2181 TAG['cas:authenticationSuccess'](
2182 TAG['cas:user'](username),
2183 *[TAG['cas:' + field.name](user[field.name])
2184 for field in self.table_user()
2185 if field.readable]))
2186 else:
2187 if version == 1:
2188 message = 'no\n'
2189 elif row:
2190 message = build_response(TAG['cas:authenticationFailure']())
2191 else:
2192 message = build_response(
2193 TAG['cas:authenticationFailure'](
2194 'Ticket %s not recognized' % ticket,
2195 _code='INVALID TICKET'))
2196 raise HTTP(200, message)
2197
2205 """
2206 returns a login form
2207
2208 method: Auth.login([next=DEFAULT [, onvalidation=DEFAULT
2209 [, onaccept=DEFAULT [, log=DEFAULT]]]])
2210
2211 """
2212
2213 table_user = self.table_user()
2214 settings = self.settings
2215 if 'username' in table_user.fields or \
2216 not settings.login_email_validate:
2217 tmpvalidator = IS_NOT_EMPTY(error_message=self.messages.is_empty)
2218 if not settings.username_case_sensitive:
2219 tmpvalidator = [IS_LOWER(), tmpvalidator]
2220 else:
2221 tmpvalidator = IS_EMAIL(error_message=self.messages.invalid_email)
2222 if not settings.email_case_sensitive:
2223 tmpvalidator = [IS_LOWER(), tmpvalidator]
2224
2225 request = current.request
2226 response = current.response
2227 session = current.session
2228
2229 passfield = settings.password_field
2230 try:
2231 table_user[passfield].requires[-1].min_length = 0
2232 except:
2233 pass
2234
2235
2236 snext = self.get_vars_next()
2237 if snext:
2238 session._auth_next = snext
2239 elif session._auth_next:
2240 snext = session._auth_next
2241
2242
2243 if next is DEFAULT:
2244
2245 next = settings.login_next
2246 user_next = snext
2247 if user_next:
2248 external = user_next.split('://')
2249 if external[0].lower() in ['http', 'https', 'ftp']:
2250 host_next = user_next.split('//', 1)[-1].split('/')[0]
2251 if host_next in settings.cas_domains:
2252 next = user_next
2253 else:
2254 next = user_next
2255 if onvalidation is DEFAULT:
2256 onvalidation = settings.login_onvalidation
2257 if onaccept is DEFAULT:
2258 onaccept = settings.login_onaccept
2259 if log is DEFAULT:
2260 log = self.messages['login_log']
2261
2262 onfail = settings.login_onfail
2263
2264 user = None
2265
2266
2267
2268 multi_login = False
2269 if self.settings.login_userfield:
2270 username = self.settings.login_userfield
2271 else:
2272 if 'username' in table_user.fields:
2273 username = 'username'
2274 else:
2275 username = 'email'
2276 if self.settings.multi_login:
2277 multi_login = True
2278 old_requires = table_user[username].requires
2279 table_user[username].requires = tmpvalidator
2280
2281
2282 if settings.login_form == self:
2283 form = SQLFORM(
2284 table_user,
2285 fields=[username, passfield],
2286 hidden=dict(_next=next),
2287 showid=settings.showid,
2288 submit_button=self.messages.login_button,
2289 delete_label=self.messages.delete_label,
2290 formstyle=settings.formstyle,
2291 separator=settings.label_separator
2292 )
2293
2294 if settings.remember_me_form:
2295
2296 if settings.formstyle != 'bootstrap':
2297 addrow(form, XML(" "),
2298 DIV(XML(" "),
2299 INPUT(_type='checkbox',
2300 _class='checkbox',
2301 _id="auth_user_remember",
2302 _name="remember",
2303 ),
2304 XML(" "),
2305 LABEL(
2306 self.messages.label_remember_me,
2307 _for="auth_user_remember",
2308 )), "",
2309 settings.formstyle,
2310 'auth_user_remember__row')
2311 elif settings.formstyle == 'bootstrap':
2312 addrow(form,
2313 "",
2314 LABEL(
2315 INPUT(_type='checkbox',
2316 _id="auth_user_remember",
2317 _name="remember"),
2318 self.messages.label_remember_me,
2319 _class="checkbox"),
2320 "",
2321 settings.formstyle,
2322 'auth_user_remember__row')
2323
2324 captcha = settings.login_captcha or \
2325 (settings.login_captcha != False and settings.captcha)
2326 if captcha:
2327 addrow(form, captcha.label, captcha, captcha.comment,
2328 settings.formstyle, 'captcha__row')
2329 accepted_form = False
2330
2331 if form.accepts(request, session if self.csrf_prevention else None,
2332 formname='login', dbio=False,
2333 onvalidation=onvalidation,
2334 hideerror=settings.hideerror):
2335
2336 accepted_form = True
2337
2338 entered_username = form.vars[username]
2339 if multi_login and '@' in entered_username:
2340
2341 user = table_user(email = entered_username)
2342 else:
2343 user = table_user(**{username: entered_username})
2344 if user:
2345
2346 temp_user = user
2347 if temp_user.registration_key == 'pending':
2348 response.flash = self.messages.registration_pending
2349 return form
2350 elif temp_user.registration_key in ('disabled', 'blocked'):
2351 response.flash = self.messages.login_disabled
2352 return form
2353 elif (not temp_user.registration_key is None
2354 and temp_user.registration_key.strip()):
2355 response.flash = \
2356 self.messages.registration_verifying
2357 return form
2358
2359
2360 user = None
2361 for login_method in settings.login_methods:
2362 if login_method != self and \
2363 login_method(request.vars[username],
2364 request.vars[passfield]):
2365 if not self in settings.login_methods:
2366
2367 form.vars[passfield] = None
2368 user = self.get_or_create_user(
2369 form.vars, settings.update_fields)
2370 break
2371 if not user:
2372
2373 if settings.login_methods[0] == self:
2374
2375 if form.vars.get(passfield, '') == temp_user[passfield]:
2376
2377 user = temp_user
2378 else:
2379
2380 if not settings.alternate_requires_registration:
2381
2382 for login_method in settings.login_methods:
2383 if login_method != self and \
2384 login_method(request.vars[username],
2385 request.vars[passfield]):
2386 if not self in settings.login_methods:
2387
2388 form.vars[passfield] = None
2389 user = self.get_or_create_user(
2390 form.vars, settings.update_fields)
2391 break
2392 if not user:
2393 self.log_event(self.messages['login_failed_log'],
2394 request.post_vars)
2395
2396 session.flash = self.messages.invalid_login
2397 callback(onfail, None)
2398 redirect(
2399 self.url(args=request.args, vars=request.get_vars),
2400 client_side=settings.client_side)
2401
2402 else:
2403
2404 cas = settings.login_form
2405 cas_user = cas.get_user()
2406
2407 if cas_user:
2408 cas_user[passfield] = None
2409 user = self.get_or_create_user(
2410 table_user._filter_fields(cas_user),
2411 settings.update_fields)
2412 elif hasattr(cas, 'login_form'):
2413 return cas.login_form()
2414 else:
2415
2416 next = self.url(settings.function, args='login')
2417 redirect(cas.login_url(next),
2418 client_side=settings.client_side)
2419
2420
2421 if user:
2422 user = Row(table_user._filter_fields(user, id=True))
2423
2424
2425 self.login_user(user)
2426 session.auth.expiration = \
2427 request.vars.get('remember', False) and \
2428 settings.long_expiration or \
2429 settings.expiration
2430 session.auth.remember = 'remember' in request.vars
2431 self.log_event(log, user)
2432 session.flash = self.messages.logged_in
2433
2434
2435 if settings.login_form == self:
2436 if accepted_form:
2437 callback(onaccept, form)
2438 if next == session._auth_next:
2439 session._auth_next = None
2440 next = replace_id(next, form)
2441 redirect(next, client_side=settings.client_side)
2442
2443 table_user[username].requires = old_requires
2444 return form
2445 elif user:
2446 callback(onaccept, None)
2447
2448 if next == session._auth_next:
2449 del session._auth_next
2450 redirect(next, client_side=settings.client_side)
2451
2483
2491 """
2492 returns a registration form
2493
2494 method: Auth.register([next=DEFAULT [, onvalidation=DEFAULT
2495 [, onaccept=DEFAULT [, log=DEFAULT]]]])
2496
2497 """
2498
2499 table_user = self.table_user()
2500 request = current.request
2501 response = current.response
2502 session = current.session
2503 if self.is_logged_in():
2504 redirect(self.settings.logged_url,
2505 client_side=self.settings.client_side)
2506 if next is DEFAULT:
2507 next = self.get_vars_next() or self.settings.register_next
2508 if onvalidation is DEFAULT:
2509 onvalidation = self.settings.register_onvalidation
2510 if onaccept is DEFAULT:
2511 onaccept = self.settings.register_onaccept
2512 if log is DEFAULT:
2513 log = self.messages['register_log']
2514
2515 table_user = self.table_user()
2516 if self.settings.login_userfield:
2517 username = self.settings.login_userfield
2518 elif 'username' in table_user.fields:
2519 username = 'username'
2520 else:
2521 username = 'email'
2522
2523
2524 unique_validator = IS_NOT_IN_DB(self.db, table_user[username])
2525 if not table_user[username].requires:
2526 table_user[username].requires = unique_validator
2527 elif isinstance(table_user[username].requires, (list, tuple)):
2528 if not any([isinstance(validator, IS_NOT_IN_DB) for validator in
2529 table_user[username].requires]):
2530 if isinstance(table_user[username].requires, list):
2531 table_user[username].requires.append(unique_validator)
2532 else:
2533 table_user[username].requires += (unique_validator, )
2534 elif not isinstance(table_user[username].requires, IS_NOT_IN_DB):
2535 table_user[username].requires = [table_user[username].requires,
2536 unique_validator]
2537
2538 passfield = self.settings.password_field
2539 formstyle = self.settings.formstyle
2540 form = SQLFORM(table_user,
2541 fields=self.settings.register_fields,
2542 hidden=dict(_next=next),
2543 showid=self.settings.showid,
2544 submit_button=self.messages.register_button,
2545 delete_label=self.messages.delete_label,
2546 formstyle=formstyle,
2547 separator=self.settings.label_separator
2548 )
2549 if self.settings.register_verify_password:
2550 for i, row in enumerate(form[0].components):
2551 item = row.element('input', _name=passfield)
2552 if item:
2553 form.custom.widget.password_two = \
2554 INPUT(_name="password_two", _type="password",
2555 _class="password",
2556 requires=IS_EXPR(
2557 'value==%s' %
2558 repr(request.vars.get(passfield, None)),
2559 error_message=self.messages.mismatched_password))
2560
2561 if formstyle == 'bootstrap':
2562 form.custom.widget.password_two[
2563 '_class'] = 'span4'
2564
2565 addrow(
2566 form, self.messages.verify_password +
2567 self.settings.label_separator,
2568 form.custom.widget.password_two,
2569 self.messages.verify_password_comment,
2570 formstyle,
2571 '%s_%s__row' % (table_user, 'password_two'),
2572 position=i + 1)
2573 break
2574 captcha = self.settings.register_captcha or self.settings.captcha
2575 if captcha:
2576 addrow(form, captcha.label, captcha,
2577 captcha.comment, self.settings.formstyle, 'captcha__row')
2578
2579
2580 if self.settings.pre_registration_div:
2581 addrow(form, '',
2582 DIV(_id="pre-reg", *self.settings.pre_registration_div),
2583 '', formstyle, '')
2584
2585 table_user.registration_key.default = key = web2py_uuid()
2586 if form.accepts(request, session if self.csrf_prevention else None,
2587 formname='register',
2588 onvalidation=onvalidation,
2589 hideerror=self.settings.hideerror):
2590 description = self.messages.group_description % form.vars
2591 if self.settings.create_user_groups:
2592 group_id = self.add_group(
2593 self.settings.create_user_groups % form.vars, description)
2594 self.add_membership(group_id, form.vars.id)
2595 if self.settings.everybody_group_id:
2596 self.add_membership(
2597 self.settings.everybody_group_id, form.vars.id)
2598 if self.settings.registration_requires_verification:
2599 link = self.url(
2600 self.settings.function, args=('verify_email', key), scheme=True)
2601 d = dict(request.vars)
2602 d.update(dict(key=key, link=link,username=form.vars[username]))
2603 if not (self.settings.mailer and self.settings.mailer.send(
2604 to=form.vars.email,
2605 subject=self.messages.verify_email_subject,
2606 message=self.messages.verify_email % d)):
2607 self.db.rollback()
2608 response.flash = self.messages.unable_send_email
2609 return form
2610 session.flash = self.messages.email_sent
2611 if self.settings.registration_requires_approval and \
2612 not self.settings.registration_requires_verification:
2613 table_user[form.vars.id] = dict(registration_key='pending')
2614 session.flash = self.messages.registration_pending
2615 elif (not self.settings.registration_requires_verification or
2616 self.settings.login_after_registration):
2617 if not self.settings.registration_requires_verification:
2618 table_user[form.vars.id] = dict(registration_key='')
2619 session.flash = self.messages.registration_successful
2620 user = table_user(**{username: form.vars[username]})
2621 self.login_user(user)
2622 session.flash = self.messages.logged_in
2623 self.log_event(log, form.vars)
2624 callback(onaccept, form)
2625 if not next:
2626 next = self.url(args=request.args)
2627 else:
2628 next = replace_id(next, form)
2629 redirect(next, client_side=self.settings.client_side)
2630 return form
2631
2633 """
2634 checks if the user is logged in and returns True/False.
2635 if so user is in auth.user as well as in session.auth.user
2636 """
2637
2638 if self.user:
2639 return True
2640 return False
2641
2679
2687 """
2688 returns a form to retrieve the user username
2689 (only if there is a username field)
2690
2691 method: Auth.retrieve_username([next=DEFAULT
2692 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2693
2694 """
2695
2696 table_user = self.table_user()
2697 if not 'username' in table_user.fields:
2698 raise HTTP(404)
2699 request = current.request
2700 response = current.response
2701 session = current.session
2702 captcha = self.settings.retrieve_username_captcha or \
2703 (self.settings.retrieve_username_captcha != False and self.settings.captcha)
2704 if not self.settings.mailer:
2705 response.flash = self.messages.function_disabled
2706 return ''
2707 if next is DEFAULT:
2708 next = self.get_vars_next() or self.settings.retrieve_username_next
2709 if onvalidation is DEFAULT:
2710 onvalidation = self.settings.retrieve_username_onvalidation
2711 if onaccept is DEFAULT:
2712 onaccept = self.settings.retrieve_username_onaccept
2713 if log is DEFAULT:
2714 log = self.messages['retrieve_username_log']
2715 old_requires = table_user.email.requires
2716 table_user.email.requires = [IS_IN_DB(self.db, table_user.email,
2717 error_message=self.messages.invalid_email)]
2718 form = SQLFORM(table_user,
2719 fields=['email'],
2720 hidden=dict(_next=next),
2721 showid=self.settings.showid,
2722 submit_button=self.messages.submit_button,
2723 delete_label=self.messages.delete_label,
2724 formstyle=self.settings.formstyle,
2725 separator=self.settings.label_separator
2726 )
2727 if captcha:
2728 addrow(form, captcha.label, captcha,
2729 captcha.comment, self.settings.formstyle, 'captcha__row')
2730
2731 if form.accepts(request, session if self.csrf_prevention else None,
2732 formname='retrieve_username', dbio=False,
2733 onvalidation=onvalidation, hideerror=self.settings.hideerror):
2734 users = table_user._db(table_user.email==form.vars.email).select()
2735 if not users:
2736 current.session.flash = \
2737 self.messages.invalid_email
2738 redirect(self.url(args=request.args))
2739 username = ', '.join(u.username for u in users)
2740 self.settings.mailer.send(to=form.vars.email,
2741 subject=self.messages.retrieve_username_subject,
2742 message=self.messages.retrieve_username
2743 % dict(username=username))
2744 session.flash = self.messages.email_sent
2745 for user in users:
2746 self.log_event(log, user)
2747 callback(onaccept, form)
2748 if not next:
2749 next = self.url(args=request.args)
2750 else:
2751 next = replace_id(next, form)
2752 redirect(next)
2753 table_user.email.requires = old_requires
2754 return form
2755
2757 import string
2758 import random
2759 password = ''
2760 specials = r'!#$*'
2761 for i in range(0, 3):
2762 password += random.choice(string.lowercase)
2763 password += random.choice(string.uppercase)
2764 password += random.choice(string.digits)
2765 password += random.choice(specials)
2766 return ''.join(random.sample(password, len(password)))
2767
2775 """
2776 returns a form to reset the user password (deprecated)
2777
2778 method: Auth.reset_password_deprecated([next=DEFAULT
2779 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2780
2781 """
2782
2783 table_user = self.table_user()
2784 request = current.request
2785 response = current.response
2786 session = current.session
2787 if not self.settings.mailer:
2788 response.flash = self.messages.function_disabled
2789 return ''
2790 if next is DEFAULT:
2791 next = self.get_vars_next() or self.settings.retrieve_password_next
2792 if onvalidation is DEFAULT:
2793 onvalidation = self.settings.retrieve_password_onvalidation
2794 if onaccept is DEFAULT:
2795 onaccept = self.settings.retrieve_password_onaccept
2796 if log is DEFAULT:
2797 log = self.messages['retrieve_password_log']
2798 old_requires = table_user.email.requires
2799 table_user.email.requires = [IS_IN_DB(self.db, table_user.email,
2800 error_message=self.messages.invalid_email)]
2801 form = SQLFORM(table_user,
2802 fields=['email'],
2803 hidden=dict(_next=next),
2804 showid=self.settings.showid,
2805 submit_button=self.messages.submit_button,
2806 delete_label=self.messages.delete_label,
2807 formstyle=self.settings.formstyle,
2808 separator=self.settings.label_separator
2809 )
2810 if form.accepts(request, session if self.csrf_prevention else None,
2811 formname='retrieve_password', dbio=False,
2812 onvalidation=onvalidation, hideerror=self.settings.hideerror):
2813 user = table_user(email=form.vars.email)
2814 if not user:
2815 current.session.flash = \
2816 self.messages.invalid_email
2817 redirect(self.url(args=request.args))
2818 elif user.registration_key in ('pending', 'disabled', 'blocked'):
2819 current.session.flash = \
2820 self.messages.registration_pending
2821 redirect(self.url(args=request.args))
2822 password = self.random_password()
2823 passfield = self.settings.password_field
2824 d = {
2825 passfield: str(table_user[passfield].validate(password)[0]),
2826 'registration_key': ''
2827 }
2828 user.update_record(**d)
2829 if self.settings.mailer and \
2830 self.settings.mailer.send(to=form.vars.email,
2831 subject=self.messages.retrieve_password_subject,
2832 message=self.messages.retrieve_password
2833 % dict(password=password)):
2834 session.flash = self.messages.email_sent
2835 else:
2836 session.flash = self.messages.unable_to_send_email
2837 self.log_event(log, user)
2838 callback(onaccept, form)
2839 if not next:
2840 next = self.url(args=request.args)
2841 else:
2842 next = replace_id(next, form)
2843 redirect(next)
2844 table_user.email.requires = old_requires
2845 return form
2846
2854 """
2855 returns a form to reset the user password
2856
2857 method: Auth.reset_password([next=DEFAULT
2858 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2859
2860 """
2861
2862 table_user = self.table_user()
2863 request = current.request
2864
2865 session = current.session
2866
2867 if next is DEFAULT:
2868 next = self.get_vars_next() or self.settings.reset_password_next
2869 try:
2870 key = request.vars.key or getarg(-1)
2871 t0 = int(key.split('-')[0])
2872 if time.time() - t0 > 60 * 60 * 24:
2873 raise Exception
2874 user = table_user(reset_password_key=key)
2875 if not user:
2876 raise Exception
2877 except Exception:
2878 session.flash = self.messages.invalid_reset_password
2879 redirect(next, client_side=self.settings.client_side)
2880 passfield = self.settings.password_field
2881 form = SQLFORM.factory(
2882 Field('new_password', 'password',
2883 label=self.messages.new_password,
2884 requires=self.table_user()[passfield].requires),
2885 Field('new_password2', 'password',
2886 label=self.messages.verify_password,
2887 requires=[IS_EXPR(
2888 'value==%s' % repr(request.vars.new_password),
2889 self.messages.mismatched_password)]),
2890 submit_button=self.messages.password_reset_button,
2891 hidden=dict(_next=next),
2892 formstyle=self.settings.formstyle,
2893 separator=self.settings.label_separator
2894 )
2895 if form.accepts(request, session,
2896 hideerror=self.settings.hideerror):
2897 user.update_record(
2898 **{passfield: str(form.vars.new_password),
2899 'registration_key': '',
2900 'reset_password_key': ''})
2901 session.flash = self.messages.password_changed
2902 if self.settings.login_after_password_change:
2903 self.login_user(user)
2904 redirect(next, client_side=self.settings.client_side)
2905 return form
2906
2914 """
2915 returns a form to reset the user password
2916
2917 method: Auth.reset_password([next=DEFAULT
2918 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2919
2920 """
2921 table_user = self.table_user()
2922 request = current.request
2923 response = current.response
2924 session = current.session
2925 captcha = self.settings.retrieve_password_captcha or \
2926 (self.settings.retrieve_password_captcha != False and self.settings.captcha)
2927
2928 if next is DEFAULT:
2929 next = self.get_vars_next() or self.settings.request_reset_password_next
2930 if not self.settings.mailer:
2931 response.flash = self.messages.function_disabled
2932 return ''
2933 if onvalidation is DEFAULT:
2934 onvalidation = self.settings.reset_password_onvalidation
2935 if onaccept is DEFAULT:
2936 onaccept = self.settings.reset_password_onaccept
2937 if log is DEFAULT:
2938 log = self.messages['reset_password_log']
2939 userfield = self.settings.login_userfield or 'username' \
2940 if 'username' in table_user.fields else 'email'
2941 if userfield=='email':
2942 table_user.email.requires = [
2943 IS_EMAIL(error_message=self.messages.invalid_email),
2944 IS_IN_DB(self.db, table_user.email,
2945 error_message=self.messages.invalid_email)]
2946 else:
2947 table_user.username.requires = [
2948 IS_IN_DB(self.db, table_user.username,
2949 error_message=self.messages.invalid_username)]
2950 form = SQLFORM(table_user,
2951 fields=[userfield],
2952 hidden=dict(_next=next),
2953 showid=self.settings.showid,
2954 submit_button=self.messages.password_reset_button,
2955 delete_label=self.messages.delete_label,
2956 formstyle=self.settings.formstyle,
2957 separator=self.settings.label_separator
2958 )
2959 if captcha:
2960 addrow(form, captcha.label, captcha,
2961 captcha.comment, self.settings.formstyle, 'captcha__row')
2962 if form.accepts(request, session if self.csrf_prevention else None,
2963 formname='reset_password', dbio=False,
2964 onvalidation=onvalidation,
2965 hideerror=self.settings.hideerror):
2966 user = table_user(**{userfield:form.vars.get(userfield)})
2967 if not user:
2968 session.flash = self.messages['invalid_%s' % userfield]
2969 redirect(self.url(args=request.args),
2970 client_side=self.settings.client_side)
2971 elif user.registration_key in ('pending', 'disabled', 'blocked'):
2972 session.flash = self.messages.registration_pending
2973 redirect(self.url(args=request.args),
2974 client_side=self.settings.client_side)
2975 if self.email_reset_password(user):
2976 session.flash = self.messages.email_sent
2977 else:
2978 session.flash = self.messages.unable_to_send_email
2979 self.log_event(log, user)
2980 callback(onaccept, form)
2981 if not next:
2982 next = self.url(args=request.args)
2983 else:
2984 next = replace_id(next, form)
2985 redirect(next, client_side=self.settings.client_side)
2986
2987 return form
2988
2990 reset_password_key = str(int(time.time())) + '-' + web2py_uuid()
2991 link = self.url(self.settings.function,
2992 args=('reset_password', reset_password_key),
2993 scheme=True)
2994 d = dict(user)
2995 d.update(dict(key=reset_password_key, link=link))
2996 if self.settings.mailer and self.settings.mailer.send(
2997 to=user.email,
2998 subject=self.messages.reset_password_subject,
2999 message=self.messages.reset_password % d):
3000 user.update_record(reset_password_key=reset_password_key)
3001 return True
3002 return False
3003
3015
3023 """
3024 returns a form that lets the user change password
3025
3026 method: Auth.change_password([next=DEFAULT[, onvalidation=DEFAULT[,
3027 onaccept=DEFAULT[, log=DEFAULT]]]])
3028 """
3029
3030 if not self.is_logged_in():
3031 redirect(self.settings.login_url,
3032 client_side=self.settings.client_side)
3033 db = self.db
3034 table_user = self.table_user()
3035 s = db(table_user.id == self.user.id)
3036
3037 request = current.request
3038 session = current.session
3039 if next is DEFAULT:
3040 next = self.get_vars_next() or self.settings.change_password_next
3041 if onvalidation is DEFAULT:
3042 onvalidation = self.settings.change_password_onvalidation
3043 if onaccept is DEFAULT:
3044 onaccept = self.settings.change_password_onaccept
3045 if log is DEFAULT:
3046 log = self.messages['change_password_log']
3047 passfield = self.settings.password_field
3048 form = SQLFORM.factory(
3049 Field('old_password', 'password',
3050 label=self.messages.old_password,
3051 requires=table_user[passfield].requires),
3052 Field('new_password', 'password',
3053 label=self.messages.new_password,
3054 requires=table_user[passfield].requires),
3055 Field('new_password2', 'password',
3056 label=self.messages.verify_password,
3057 requires=[IS_EXPR(
3058 'value==%s' % repr(request.vars.new_password),
3059 self.messages.mismatched_password)]),
3060 submit_button=self.messages.password_change_button,
3061 hidden=dict(_next=next),
3062 formstyle=self.settings.formstyle,
3063 separator=self.settings.label_separator
3064 )
3065 if form.accepts(request, session,
3066 formname='change_password',
3067 onvalidation=onvalidation,
3068 hideerror=self.settings.hideerror):
3069
3070 if not form.vars['old_password'] == s.select(limitby=(0,1), orderby_on_limitby=False).first()[passfield]:
3071 form.errors['old_password'] = self.messages.invalid_password
3072 else:
3073 d = {passfield: str(form.vars.new_password)}
3074 s.update(**d)
3075 session.flash = self.messages.password_changed
3076 self.log_event(log, self.user)
3077 callback(onaccept, form)
3078 if not next:
3079 next = self.url(args=request.args)
3080 else:
3081 next = replace_id(next, form)
3082 redirect(next, client_side=self.settings.client_side)
3083 return form
3084
3092 """
3093 returns a form that lets the user change his/her profile
3094
3095 method: Auth.profile([next=DEFAULT [, onvalidation=DEFAULT
3096 [, onaccept=DEFAULT [, log=DEFAULT]]]])
3097
3098 """
3099
3100 table_user = self.table_user()
3101 if not self.is_logged_in():
3102 redirect(self.settings.login_url,
3103 client_side=self.settings.client_side)
3104 passfield = self.settings.password_field
3105 table_user[passfield].writable = False
3106 request = current.request
3107 session = current.session
3108 if next is DEFAULT:
3109 next = self.get_vars_next() or self.settings.profile_next
3110 if onvalidation is DEFAULT:
3111 onvalidation = self.settings.profile_onvalidation
3112 if onaccept is DEFAULT:
3113 onaccept = self.settings.profile_onaccept
3114 if log is DEFAULT:
3115 log = self.messages['profile_log']
3116 form = SQLFORM(
3117 table_user,
3118 self.user.id,
3119 fields=self.settings.profile_fields,
3120 hidden=dict(_next=next),
3121 showid=self.settings.showid,
3122 submit_button=self.messages.profile_save_button,
3123 delete_label=self.messages.delete_label,
3124 upload=self.settings.download_url,
3125 formstyle=self.settings.formstyle,
3126 separator=self.settings.label_separator,
3127 deletable=self.settings.allow_delete_accounts,
3128 )
3129 if form.accepts(request, session,
3130 formname='profile',
3131 onvalidation=onvalidation,
3132 hideerror=self.settings.hideerror):
3133 self.user.update(table_user._filter_fields(form.vars))
3134 session.flash = self.messages.profile_updated
3135 self.log_event(log, self.user)
3136 callback(onaccept, form)
3137 if form.deleted:
3138 return self.logout()
3139 if not next:
3140 next = self.url(args=request.args)
3141 else:
3142 next = replace_id(next, form)
3143 redirect(next, client_side=self.settings.client_side)
3144 return form
3145
3147 onaccept = self.settings.login_onaccept
3148 if onaccept:
3149 form = Storage(dict(vars=self.user))
3150 if not isinstance(onaccept,(list, tuple)):
3151 onaccept = [onaccept]
3152 for callback in onaccept:
3153 callback(form)
3154
3157
3159 """
3160 usage: POST TO http://..../impersonate request.post_vars.user_id=<id>
3161 set request.post_vars.user_id to 0 to restore original user.
3162
3163 requires impersonator is logged in and
3164 has_permission('impersonate', 'auth_user', user_id)
3165 """
3166 request = current.request
3167 session = current.session
3168 auth = session.auth
3169 table_user = self.table_user()
3170 if not self.is_logged_in():
3171 raise HTTP(401, "Not Authorized")
3172 current_id = auth.user.id
3173 requested_id = user_id
3174 if user_id is DEFAULT:
3175 user_id = current.request.post_vars.user_id
3176 if user_id and user_id != self.user.id and user_id != '0':
3177 if not self.has_permission('impersonate',
3178 self.table_user(),
3179 user_id):
3180 raise HTTP(403, "Forbidden")
3181 user = table_user(user_id)
3182 if not user:
3183 raise HTTP(401, "Not Authorized")
3184 auth.impersonator = cPickle.dumps(session)
3185 auth.user.update(
3186 table_user._filter_fields(user, True))
3187 self.user = auth.user
3188 self.update_groups()
3189 log = self.messages['impersonate_log']
3190 self.log_event(log, dict(id=current_id, other_id=auth.user.id))
3191 self.run_login_onaccept()
3192 elif user_id in (0, '0'):
3193 if self.is_impersonating():
3194 session.clear()
3195 session.update(cPickle.loads(auth.impersonator))
3196 self.user = session.auth.user
3197 self.update_groups()
3198 self.run_login_onaccept()
3199 return None
3200 if requested_id is DEFAULT and not request.post_vars:
3201 return SQLFORM.factory(Field('user_id', 'integer'))
3202 return SQLFORM(table_user, user.id, readonly=True)
3203
3218
3240
3242 """
3243 you can change the view for this page to make it look as you like
3244 """
3245 if current.request.ajax:
3246 raise HTTP(403, 'ACCESS DENIED')
3247 return self.messages.access_denied
3248
3249 - def requires(self, condition, requires_login=True, otherwise=None):
3250 """
3251 decorator that prevents access to action if not logged in
3252 """
3253
3254 def decorator(action):
3255
3256 def f(*a, **b):
3257
3258 basic_allowed, basic_accepted, user = self.basic()
3259 user = user or self.user
3260 if requires_login:
3261 if not user:
3262 if current.request.ajax:
3263 raise HTTP(401, self.messages.ajax_failed_authentication)
3264 elif not otherwise is None:
3265 if callable(otherwise):
3266 return otherwise()
3267 redirect(otherwise)
3268 elif self.settings.allow_basic_login_only or \
3269 basic_accepted or current.request.is_restful:
3270 raise HTTP(403, "Not authorized")
3271 else:
3272 next = self.here()
3273 current.session.flash = current.response.flash
3274 return call_or_redirect(
3275 self.settings.on_failed_authentication,
3276 self.settings.login_url +
3277 '?_next=' + urllib.quote(next))
3278
3279 if callable(condition):
3280 flag = condition()
3281 else:
3282 flag = condition
3283 if not flag:
3284 current.session.flash = self.messages.access_denied
3285 return call_or_redirect(
3286 self.settings.on_failed_authorization)
3287 return action(*a, **b)
3288 f.__doc__ = action.__doc__
3289 f.__name__ = action.__name__
3290 f.__dict__.update(action.__dict__)
3291 return f
3292
3293 return decorator
3294
3296 """
3297 decorator that prevents access to action if not logged in
3298 """
3299 return self.requires(True, otherwise=otherwise)
3300
3302 """
3303 decorator that prevents access to action if not logged in or
3304 if user logged in is not a member of group_id.
3305 If role is provided instead of group_id then the
3306 group_id is calculated.
3307 """
3308 def has_membership(self=self, group_id=group_id, role=role):
3309 return self.has_membership(group_id=group_id, role=role)
3310 return self.requires(has_membership, otherwise=otherwise)
3311
3314 """
3315 decorator that prevents access to action if not logged in or
3316 if user logged in is not a member of any group (role) that
3317 has 'name' access to 'table_name', 'record_id'.
3318 """
3319 def has_permission(self=self, name=name, table_name=table_name, record_id=record_id):
3320 return self.has_permission(name, table_name, record_id)
3321 return self.requires(has_permission, otherwise=otherwise)
3322
3324 """
3325 decorator that prevents access to action if not logged in or
3326 if user logged in is not a member of group_id.
3327 If role is provided instead of group_id then the
3328 group_id is calculated.
3329 """
3330 def verify():
3331 return URL.verify(current.request, user_signature=True, hash_vars=hash_vars)
3332 return self.requires(verify, otherwise)
3333
3335 """
3336 creates a group associated to a role
3337 """
3338
3339 group_id = self.table_group().insert(
3340 role=role, description=description)
3341 self.log_event(self.messages['add_group_log'],
3342 dict(group_id=group_id, role=role))
3343 return group_id
3344
3354
3356 """
3357 returns the group_id of the group specified by the role
3358 """
3359 rows = self.db(self.table_group().role == role).select()
3360 if not rows:
3361 return None
3362 return rows[0].id
3363
3365 """
3366 returns the group_id of the group uniquely associated to this user
3367 i.e. role=user:[user_id]
3368 """
3369 return self.id_group(self.user_group_role(user_id))
3370
3372 if not self.settings.create_user_groups:
3373 return None
3374 if user_id:
3375 user = self.table_user()[user_id]
3376 else:
3377 user = self.user
3378 return self.settings.create_user_groups % user
3379
3381 """
3382 checks if user is member of group_id or role
3383 """
3384
3385 group_id = group_id or self.id_group(role)
3386 try:
3387 group_id = int(group_id)
3388 except:
3389 group_id = self.id_group(group_id)
3390 if not user_id and self.user:
3391 user_id = self.user.id
3392 membership = self.table_membership()
3393 if group_id and user_id and self.db((membership.user_id == user_id)
3394 & (membership.group_id == group_id)).select():
3395 r = True
3396 else:
3397 r = False
3398 self.log_event(self.messages['has_membership_log'],
3399 dict(user_id=user_id, group_id=group_id, check=r))
3400 return r
3401
3403 """
3404 gives user_id membership of group_id or role
3405 if user is None than user_id is that of current logged in user
3406 """
3407
3408 group_id = group_id or self.id_group(role)
3409 try:
3410 group_id = int(group_id)
3411 except:
3412 group_id = self.id_group(group_id)
3413 if not user_id and self.user:
3414 user_id = self.user.id
3415 membership = self.table_membership()
3416 record = membership(user_id=user_id, group_id=group_id)
3417 if record:
3418 return record.id
3419 else:
3420 id = membership.insert(group_id=group_id, user_id=user_id)
3421 self.update_groups()
3422 self.log_event(self.messages['add_membership_log'],
3423 dict(user_id=user_id, group_id=group_id))
3424 return id
3425
3427 """
3428 revokes membership from group_id to user_id
3429 if user_id is None than user_id is that of current logged in user
3430 """
3431
3432 group_id = group_id or self.id_group(role)
3433 if not user_id and self.user:
3434 user_id = self.user.id
3435 membership = self.table_membership()
3436 self.log_event(self.messages['del_membership_log'],
3437 dict(user_id=user_id, group_id=group_id))
3438 ret = self.db(membership.user_id
3439 == user_id)(membership.group_id
3440 == group_id).delete()
3441 self.update_groups()
3442 return ret
3443
3444 - def has_permission(
3445 self,
3446 name='any',
3447 table_name='',
3448 record_id=0,
3449 user_id=None,
3450 group_id=None,
3451 ):
3452 """
3453 checks if user_id or current logged in user is member of a group
3454 that has 'name' permission on 'table_name' and 'record_id'
3455 if group_id is passed, it checks whether the group has the permission
3456 """
3457
3458 if not group_id and self.settings.everybody_group_id and \
3459 self.has_permission(
3460 name, table_name, record_id, user_id=None,
3461 group_id=self.settings.everybody_group_id):
3462 return True
3463
3464 if not user_id and not group_id and self.user:
3465 user_id = self.user.id
3466 if user_id:
3467 membership = self.table_membership()
3468 rows = self.db(membership.user_id
3469 == user_id).select(membership.group_id)
3470 groups = set([row.group_id for row in rows])
3471 if group_id and not group_id in groups:
3472 return False
3473 else:
3474 groups = set([group_id])
3475 permission = self.table_permission()
3476 rows = self.db(permission.name == name)(permission.table_name
3477 == str(table_name))(permission.record_id
3478 == record_id).select(permission.group_id)
3479 groups_required = set([row.group_id for row in rows])
3480 if record_id:
3481 rows = self.db(permission.name
3482 == name)(permission.table_name
3483 == str(table_name))(permission.record_id
3484 == 0).select(permission.group_id)
3485 groups_required = groups_required.union(set([row.group_id
3486 for row in rows]))
3487 if groups.intersection(groups_required):
3488 r = True
3489 else:
3490 r = False
3491 if user_id:
3492 self.log_event(self.messages['has_permission_log'],
3493 dict(user_id=user_id, name=name,
3494 table_name=table_name, record_id=record_id))
3495 return r
3496
3497 - def add_permission(
3498 self,
3499 group_id,
3500 name='any',
3501 table_name='',
3502 record_id=0,
3503 ):
3504 """
3505 gives group_id 'name' access to 'table_name' and 'record_id'
3506 """
3507
3508 permission = self.table_permission()
3509 if group_id == 0:
3510 group_id = self.user_group()
3511 record = self.db(permission.group_id == group_id)(permission.name == name)(permission.table_name == str(table_name))(
3512 permission.record_id == long(record_id)).select(limitby=(0,1), orderby_on_limitby=False).first()
3513 if record:
3514 id = record.id
3515 else:
3516 id = permission.insert(group_id=group_id, name=name,
3517 table_name=str(table_name),
3518 record_id=long(record_id))
3519 self.log_event(self.messages['add_permission_log'],
3520 dict(permission_id=id, group_id=group_id,
3521 name=name, table_name=table_name,
3522 record_id=record_id))
3523 return id
3524
3525 - def del_permission(
3526 self,
3527 group_id,
3528 name='any',
3529 table_name='',
3530 record_id=0,
3531 ):
3532 """
3533 revokes group_id 'name' access to 'table_name' and 'record_id'
3534 """
3535
3536 permission = self.table_permission()
3537 self.log_event(self.messages['del_permission_log'],
3538 dict(group_id=group_id, name=name,
3539 table_name=table_name, record_id=record_id))
3540 return self.db(permission.group_id == group_id)(permission.name
3541 == name)(permission.table_name
3542 == str(table_name))(permission.record_id
3543 == long(record_id)).delete()
3544
3546 """
3547 returns a query with all accessible records for user_id or
3548 the current logged in user
3549 this method does not work on GAE because uses JOIN and IN
3550
3551 example:
3552
3553 db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL)
3554
3555 """
3556 if not user_id:
3557 user_id = self.user_id
3558 db = self.db
3559 if isinstance(table, str) and table in self.db.tables():
3560 table = self.db[table]
3561 elif isinstance(table, (Set, Query)):
3562
3563 if isinstance(table, Set):
3564 cquery = table.query
3565 else:
3566 cquery = table
3567 tablenames = db._adapter.tables(cquery)
3568 for tablename in tablenames:
3569 cquery &= self.accessible_query(name, tablename,
3570 user_id=user_id)
3571 return cquery
3572 if not isinstance(table, str) and\
3573 self.has_permission(name, table, 0, user_id):
3574 return table.id > 0
3575 membership = self.table_membership()
3576 permission = self.table_permission()
3577 query = table.id.belongs(
3578 db(membership.user_id == user_id)
3579 (membership.group_id == permission.group_id)
3580 (permission.name == name)
3581 (permission.table_name == table)
3582 ._select(permission.record_id))
3583 if self.settings.everybody_group_id:
3584 query |= table.id.belongs(
3585 db(permission.group_id == self.settings.everybody_group_id)
3586 (permission.name == name)
3587 (permission.table_name == table)
3588 ._select(permission.record_id))
3589 return query
3590
3591 @staticmethod
3592 - def archive(form,
3593 archive_table=None,
3594 current_record='current_record',
3595 archive_current=False,
3596 fields=None):
3597 """
3598 If you have a table (db.mytable) that needs full revision history you can just do:
3599
3600 form=crud.update(db.mytable,myrecord,onaccept=auth.archive)
3601
3602 or
3603
3604 form=SQLFORM(db.mytable,myrecord).process(onaccept=auth.archive)
3605
3606 crud.archive will define a new table "mytable_archive" and store
3607 a copy of the current record (if archive_current=True)
3608 or a copy of the previous record (if archive_current=False)
3609 in the newly created table including a reference
3610 to the current record.
3611
3612 fields allows to specify extra fields that need to be archived.
3613
3614 If you want to access such table you need to define it yourself
3615 in a model:
3616
3617 db.define_table('mytable_archive',
3618 Field('current_record',db.mytable),
3619 db.mytable)
3620
3621 Notice such table includes all fields of db.mytable plus one: current_record.
3622 crud.archive does not timestamp the stored record unless your original table
3623 has a fields like:
3624
3625 db.define_table(...,
3626 Field('saved_on','datetime',
3627 default=request.now,update=request.now,writable=False),
3628 Field('saved_by',auth.user,
3629 default=auth.user_id,update=auth.user_id,writable=False),
3630
3631 there is nothing special about these fields since they are filled before
3632 the record is archived.
3633
3634 If you want to change the archive table name and the name of the reference field
3635 you can do, for example:
3636
3637 db.define_table('myhistory',
3638 Field('parent_record',db.mytable),
3639 db.mytable)
3640
3641 and use it as:
3642
3643 form=crud.update(db.mytable,myrecord,
3644 onaccept=lambda form:crud.archive(form,
3645 archive_table=db.myhistory,
3646 current_record='parent_record'))
3647
3648 """
3649 if not archive_current and not form.record:
3650 return None
3651 table = form.table
3652 if not archive_table:
3653 archive_table_name = '%s_archive' % table
3654 if not archive_table_name in table._db:
3655 table._db.define_table(
3656 archive_table_name,
3657 Field(current_record, table),
3658 *[field.clone(unique=False) for field in table])
3659 archive_table = table._db[archive_table_name]
3660 new_record = {current_record: form.vars.id}
3661 for fieldname in archive_table.fields:
3662 if not fieldname in ['id', current_record]:
3663 if archive_current and fieldname in form.vars:
3664 new_record[fieldname] = form.vars[fieldname]
3665 elif form.record and fieldname in form.record:
3666 new_record[fieldname] = form.record[fieldname]
3667 if fields:
3668 new_record.update(fields)
3669 id = archive_table.insert(**new_record)
3670 return id
3671
3672 - def wiki(self,
3673 slug=None,
3674 env=None,
3675 render='markmin',
3676 manage_permissions=False,
3677 force_prefix='',
3678 restrict_search=False,
3679 resolve=True,
3680 extra=None,
3681 menu_groups=None,
3682 templates=None,
3683 migrate=True,
3684 controller=None,
3685 function=None,
3686 force_render=False,
3687 groups=None):
3688
3689 if controller and function: resolve = False
3690
3691 if not hasattr(self, '_wiki'):
3692 self._wiki = Wiki(self, render=render,
3693 manage_permissions=manage_permissions,
3694 force_prefix=force_prefix,
3695 restrict_search=restrict_search,
3696 env=env, extra=extra or {},
3697 menu_groups=menu_groups,
3698 templates=templates,
3699 migrate=migrate,
3700 controller=controller,
3701 function=function,
3702 groups=groups)
3703 else:
3704 self._wiki.env.update(env or {})
3705
3706
3707
3708 wiki = None
3709 if resolve:
3710 if slug:
3711 wiki = self._wiki.read(slug, force_render)
3712 if isinstance(wiki, dict) and wiki.has_key('content'):
3713
3714 wiki = wiki['content']
3715 else:
3716 wiki = self._wiki()
3717 if isinstance(wiki, basestring):
3718 wiki = XML(wiki)
3719 return wiki
3720
3722 """to be used in menu.py for app wide wiki menus"""
3723 if (hasattr(self, "_wiki") and
3724 self._wiki.settings.controller and
3725 self._wiki.settings.function):
3726 self._wiki.automenu()
3727
3728
3729 -class Crud(object):
3730
3731 - def url(self, f=None, args=None, vars=None):
3732 """
3733 this should point to the controller that exposes
3734 download and crud
3735 """
3736 if args is None:
3737 args = []
3738 if vars is None:
3739 vars = {}
3740 return URL(c=self.settings.controller, f=f, args=args, vars=vars)
3741
3742 - def __init__(self, environment, db=None, controller='default'):
3791
3793 args = current.request.args
3794 if len(args) < 1:
3795 raise HTTP(404)
3796 elif args[0] == 'tables':
3797 return self.tables()
3798 elif len(args) > 1 and not args(1) in self.db.tables:
3799 raise HTTP(404)
3800 table = self.db[args(1)]
3801 if args[0] == 'create':
3802 return self.create(table)
3803 elif args[0] == 'select':
3804 return self.select(table, linkto=self.url(args='read'))
3805 elif args[0] == 'search':
3806 form, rows = self.search(table, linkto=self.url(args='read'))
3807 return DIV(form, SQLTABLE(rows))
3808 elif args[0] == 'read':
3809 return self.read(table, args(2))
3810 elif args[0] == 'update':
3811 return self.update(table, args(2))
3812 elif args[0] == 'delete':
3813 return self.delete(table, args(2))
3814 else:
3815 raise HTTP(404)
3816
3820
3829
3834
3835 @staticmethod
3836 - def archive(form, archive_table=None, current_record='current_record'):
3837 return Auth.archive(form, archive_table=archive_table,
3838 current_record=current_record)
3839
3840 - def update(
3841 self,
3842 table,
3843 record,
3844 next=DEFAULT,
3845 onvalidation=DEFAULT,
3846 onaccept=DEFAULT,
3847 ondelete=DEFAULT,
3848 log=DEFAULT,
3849 message=DEFAULT,
3850 deletable=DEFAULT,
3851 formname=DEFAULT,
3852 **attributes
3853 ):
3854 """
3855 method: Crud.update(table, record, [next=DEFAULT
3856 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT
3857 [, message=DEFAULT[, deletable=DEFAULT]]]]]])
3858
3859 """
3860 if not (isinstance(table, self.db.Table) or table in self.db.tables) \
3861 or (isinstance(record, str) and not str(record).isdigit()):
3862 raise HTTP(404)
3863 if not isinstance(table, self.db.Table):
3864 table = self.db[table]
3865 try:
3866 record_id = record.id
3867 except:
3868 record_id = record or 0
3869 if record_id and not self.has_permission('update', table, record_id):
3870 redirect(self.settings.auth.settings.on_failed_authorization)
3871 if not record_id and not self.has_permission('create', table, record_id):
3872 redirect(self.settings.auth.settings.on_failed_authorization)
3873
3874 request = current.request
3875 response = current.response
3876 session = current.session
3877 if request.extension == 'json' and request.vars.json:
3878 request.vars.update(json_parser.loads(request.vars.json))
3879 if next is DEFAULT:
3880 next = request.get_vars._next \
3881 or request.post_vars._next \
3882 or self.settings.update_next
3883 if onvalidation is DEFAULT:
3884 onvalidation = self.settings.update_onvalidation
3885 if onaccept is DEFAULT:
3886 onaccept = self.settings.update_onaccept
3887 if ondelete is DEFAULT:
3888 ondelete = self.settings.update_ondelete
3889 if log is DEFAULT:
3890 log = self.messages['update_log']
3891 if deletable is DEFAULT:
3892 deletable = self.settings.update_deletable
3893 if message is DEFAULT:
3894 message = self.messages.record_updated
3895 if not 'hidden' in attributes:
3896 attributes['hidden'] = {}
3897 attributes['hidden']['_next'] = next
3898 form = SQLFORM(
3899 table,
3900 record,
3901 showid=self.settings.showid,
3902 submit_button=self.messages.submit_button,
3903 delete_label=self.messages.delete_label,
3904 deletable=deletable,
3905 upload=self.settings.download_url,
3906 formstyle=self.settings.formstyle,
3907 separator=self.settings.label_separator,
3908 **attributes
3909 )
3910 self.accepted = False
3911 self.deleted = False
3912 captcha = self.settings.update_captcha or self.settings.captcha
3913 if record and captcha:
3914 addrow(form, captcha.label, captcha, captcha.comment,
3915 self.settings.formstyle, 'captcha__row')
3916 captcha = self.settings.create_captcha or self.settings.captcha
3917 if not record and captcha:
3918 addrow(form, captcha.label, captcha, captcha.comment,
3919 self.settings.formstyle, 'captcha__row')
3920 if not request.extension in ('html', 'load'):
3921 (_session, _formname) = (None, None)
3922 else:
3923 (_session, _formname) = (
3924 session, '%s/%s' % (table._tablename, form.record_id))
3925 if not formname is DEFAULT:
3926 _formname = formname
3927 keepvalues = self.settings.keepvalues
3928 if request.vars.delete_this_record:
3929 keepvalues = False
3930 if isinstance(onvalidation, StorageList):
3931 onvalidation = onvalidation.get(table._tablename, [])
3932 if form.accepts(request, _session, formname=_formname,
3933 onvalidation=onvalidation, keepvalues=keepvalues,
3934 hideerror=self.settings.hideerror,
3935 detect_record_change=self.settings.detect_record_change):
3936 self.accepted = True
3937 response.flash = message
3938 if log:
3939 self.log_event(log, form.vars)
3940 if request.vars.delete_this_record:
3941 self.deleted = True
3942 message = self.messages.record_deleted
3943 callback(ondelete, form, table._tablename)
3944 response.flash = message
3945 callback(onaccept, form, table._tablename)
3946 if not request.extension in ('html', 'load'):
3947 raise HTTP(200, 'RECORD CREATED/UPDATED')
3948 if isinstance(next, (list, tuple)):
3949 next = next[0]
3950 if next:
3951 next = replace_id(next, form)
3952 session.flash = response.flash
3953 redirect(next)
3954 elif not request.extension in ('html', 'load'):
3955 raise HTTP(401, serializers.json(dict(errors=form.errors)))
3956 return form
3957
3969 """
3970 method: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT
3971 [, onaccept=DEFAULT [, log=DEFAULT[, message=DEFAULT]]]]])
3972 """
3973
3974 if next is DEFAULT:
3975 next = self.settings.create_next
3976 if onvalidation is DEFAULT:
3977 onvalidation = self.settings.create_onvalidation
3978 if onaccept is DEFAULT:
3979 onaccept = self.settings.create_onaccept
3980 if log is DEFAULT:
3981 log = self.messages['create_log']
3982 if message is DEFAULT:
3983 message = self.messages.record_created
3984 return self.update(
3985 table,
3986 None,
3987 next=next,
3988 onvalidation=onvalidation,
3989 onaccept=onaccept,
3990 log=log,
3991 message=message,
3992 deletable=False,
3993 formname=formname,
3994 **attributes
3995 )
3996
3997 - def read(self, table, record):
3998 if not (isinstance(table, self.db.Table) or table in self.db.tables) \
3999 or (isinstance(record, str) and not str(record).isdigit()):
4000 raise HTTP(404)
4001 if not isinstance(table, self.db.Table):
4002 table = self.db[table]
4003 if not self.has_permission('read', table, record):
4004 redirect(self.settings.auth.settings.on_failed_authorization)
4005 form = SQLFORM(
4006 table,
4007 record,
4008 readonly=True,
4009 comments=False,
4010 upload=self.settings.download_url,
4011 showid=self.settings.showid,
4012 formstyle=self.settings.formstyle,
4013 separator=self.settings.label_separator
4014 )
4015 if not current.request.extension in ('html', 'load'):
4016 return table._filter_fields(form.record, id=True)
4017 return form
4018
4019 - def delete(
4020 self,
4021 table,
4022 record_id,
4023 next=DEFAULT,
4024 message=DEFAULT,
4025 ):
4026 """
4027 method: Crud.delete(table, record_id, [next=DEFAULT
4028 [, message=DEFAULT]])
4029 """
4030 if not (isinstance(table, self.db.Table) or table in self.db.tables):
4031 raise HTTP(404)
4032 if not isinstance(table, self.db.Table):
4033 table = self.db[table]
4034 if not self.has_permission('delete', table, record_id):
4035 redirect(self.settings.auth.settings.on_failed_authorization)
4036 request = current.request
4037 session = current.session
4038 if next is DEFAULT:
4039 next = request.get_vars._next \
4040 or request.post_vars._next \
4041 or self.settings.delete_next
4042 if message is DEFAULT:
4043 message = self.messages.record_deleted
4044 record = table[record_id]
4045 if record:
4046 callback(self.settings.delete_onvalidation, record)
4047 del table[record_id]
4048 callback(self.settings.delete_onaccept, record, table._tablename)
4049 session.flash = message
4050 redirect(next)
4051
4052 - def rows(
4053 self,
4054 table,
4055 query=None,
4056 fields=None,
4057 orderby=None,
4058 limitby=None,
4059 ):
4060 if not (isinstance(table, self.db.Table) or table in self.db.tables):
4061 raise HTTP(404)
4062 if not self.has_permission('select', table):
4063 redirect(self.settings.auth.settings.on_failed_authorization)
4064
4065
4066 if not isinstance(table, self.db.Table):
4067 table = self.db[table]
4068 if not query:
4069 query = table.id > 0
4070 if not fields:
4071 fields = [field for field in table if field.readable]
4072 else:
4073 fields = [table[f] if isinstance(f, str) else f for f in fields]
4074 rows = self.db(query).select(*fields, **dict(orderby=orderby,
4075 limitby=limitby))
4076 return rows
4077
4078 - def select(
4079 self,
4080 table,
4081 query=None,
4082 fields=None,
4083 orderby=None,
4084 limitby=None,
4085 headers=None,
4086 **attr
4087 ):
4088 headers = headers or {}
4089 rows = self.rows(table, query, fields, orderby, limitby)
4090 if not rows:
4091 return None
4092 if not 'upload' in attr:
4093 attr['upload'] = self.url('download')
4094 if not current.request.extension in ('html', 'load'):
4095 return rows.as_list()
4096 if not headers:
4097 if isinstance(table, str):
4098 table = self.db[table]
4099 headers = dict((str(k), k.label) for k in table)
4100 return SQLTABLE(rows, headers=headers, **attr)
4101
4108
4109 - def get_query(self, field, op, value, refsearch=False):
4110 try:
4111 if refsearch:
4112 format = self.get_format(field)
4113 if op == 'equals':
4114 if not refsearch:
4115 return field == value
4116 else:
4117 return lambda row: row[field.name][format] == value
4118 elif op == 'not equal':
4119 if not refsearch:
4120 return field != value
4121 else:
4122 return lambda row: row[field.name][format] != value
4123 elif op == 'greater than':
4124 if not refsearch:
4125 return field > value
4126 else:
4127 return lambda row: row[field.name][format] > value
4128 elif op == 'less than':
4129 if not refsearch:
4130 return field < value
4131 else:
4132 return lambda row: row[field.name][format] < value
4133 elif op == 'starts with':
4134 if not refsearch:
4135 return field.like(value + '%')
4136 else:
4137 return lambda row: str(row[field.name][format]).startswith(value)
4138 elif op == 'ends with':
4139 if not refsearch:
4140 return field.like('%' + value)
4141 else:
4142 return lambda row: str(row[field.name][format]).endswith(value)
4143 elif op == 'contains':
4144 if not refsearch:
4145 return field.like('%' + value + '%')
4146 else:
4147 return lambda row: value in row[field.name][format]
4148 except:
4149 return None
4150
4151 - def search(self, *tables, **args):
4152 """
4153 Creates a search form and its results for a table
4154 Example usage:
4155 form, results = crud.search(db.test,
4156 queries = ['equals', 'not equal', 'contains'],
4157 query_labels={'equals':'Equals',
4158 'not equal':'Not equal'},
4159 fields = ['id','children'],
4160 field_labels = {
4161 'id':'ID','children':'Children'},
4162 zero='Please choose',
4163 query = (db.test.id > 0)&(db.test.id != 3) )
4164 """
4165 table = tables[0]
4166 fields = args.get('fields', table.fields)
4167 validate = args.get('validate',True)
4168 request = current.request
4169 db = self.db
4170 if not (isinstance(table, db.Table) or table in db.tables):
4171 raise HTTP(404)
4172 attributes = {}
4173 for key in ('orderby', 'groupby', 'left', 'distinct', 'limitby', 'cache'):
4174 if key in args:
4175 attributes[key] = args[key]
4176 tbl = TABLE()
4177 selected = []
4178 refsearch = []
4179 results = []
4180 showall = args.get('showall', False)
4181 if showall:
4182 selected = fields
4183 chkall = args.get('chkall', False)
4184 if chkall:
4185 for f in fields:
4186 request.vars['chk%s' % f] = 'on'
4187 ops = args.get('queries', [])
4188 zero = args.get('zero', '')
4189 if not ops:
4190 ops = ['equals', 'not equal', 'greater than',
4191 'less than', 'starts with',
4192 'ends with', 'contains']
4193 ops.insert(0, zero)
4194 query_labels = args.get('query_labels', {})
4195 query = args.get('query', table.id > 0)
4196 field_labels = args.get('field_labels', {})
4197 for field in fields:
4198 field = table[field]
4199 if not field.readable:
4200 continue
4201 fieldname = field.name
4202 chkval = request.vars.get('chk' + fieldname, None)
4203 txtval = request.vars.get('txt' + fieldname, None)
4204 opval = request.vars.get('op' + fieldname, None)
4205 row = TR(TD(INPUT(_type="checkbox", _name="chk" + fieldname,
4206 _disabled=(field.type == 'id'),
4207 value=(field.type == 'id' or chkval == 'on'))),
4208 TD(field_labels.get(fieldname, field.label)),
4209 TD(SELECT([OPTION(query_labels.get(op, op),
4210 _value=op) for op in ops],
4211 _name="op" + fieldname,
4212 value=opval)),
4213 TD(INPUT(_type="text", _name="txt" + fieldname,
4214 _value=txtval, _id='txt' + fieldname,
4215 _class=str(field.type))))
4216 tbl.append(row)
4217 if request.post_vars and (chkval or field.type == 'id'):
4218 if txtval and opval != '':
4219 if field.type[0:10] == 'reference ':
4220 refsearch.append(self.get_query(field,
4221 opval, txtval, refsearch=True))
4222 elif validate:
4223 value, error = field.validate(txtval)
4224 if not error:
4225
4226 query &= self.get_query(field, opval, value)
4227 else:
4228 row[3].append(DIV(error, _class='error'))
4229 else:
4230 query &= self.get_query(field, opval, txtval)
4231 selected.append(field)
4232 form = FORM(tbl, INPUT(_type="submit"))
4233 if selected:
4234 try:
4235 results = db(query).select(*selected, **attributes)
4236 for r in refsearch:
4237 results = results.find(r)
4238 except:
4239 results = None
4240 return form, results
4241
4242
4243 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor()))
4244
4245
4246 -def fetch(url, data=None, headers=None,
4247 cookie=Cookie.SimpleCookie(),
4248 user_agent='Mozilla/5.0'):
4249 headers = headers or {}
4250 if not data is None:
4251 data = urllib.urlencode(data)
4252 if user_agent:
4253 headers['User-agent'] = user_agent
4254 headers['Cookie'] = ' '.join(
4255 ['%s=%s;' % (c.key, c.value) for c in cookie.values()])
4256 try:
4257 from google.appengine.api import urlfetch
4258 except ImportError:
4259 req = urllib2.Request(url, data, headers)
4260 html = urllib2.urlopen(req).read()
4261 else:
4262 method = ((data is None) and urlfetch.GET) or urlfetch.POST
4263 while url is not None:
4264 response = urlfetch.fetch(url=url, payload=data,
4265 method=method, headers=headers,
4266 allow_truncated=False, follow_redirects=False,
4267 deadline=10)
4268
4269 data = None
4270 method = urlfetch.GET
4271
4272 cookie.load(response.headers.get('set-cookie', ''))
4273 url = response.headers.get('location')
4274 html = response.content
4275 return html
4276
4277 regex_geocode = \
4278 re.compile(r"""<geometry>[\W]*?<location>[\W]*?<lat>(?P<la>[^<]*)</lat>[\W]*?<lng>(?P<lo>[^<]*)</lng>[\W]*?</location>""")
4282 try:
4283 a = urllib.quote(address)
4284 txt = fetch('http://maps.googleapis.com/maps/api/geocode/xml?sensor=false&address=%s'
4285 % a)
4286 item = regex_geocode.search(txt)
4287 (la, lo) = (float(item.group('la')), float(item.group('lo')))
4288 return (la, lo)
4289 except:
4290 return (0.0, 0.0)
4291
4294 c = f.func_code.co_argcount
4295 n = f.func_code.co_varnames[:c]
4296
4297 defaults = f.func_defaults or []
4298 pos_args = n[0:-len(defaults)]
4299 named_args = n[-len(defaults):]
4300
4301 arg_dict = {}
4302
4303
4304 for pos_index, pos_val in enumerate(a[:c]):
4305 arg_dict[n[pos_index]
4306 ] = pos_val
4307
4308
4309
4310 for arg_name in pos_args[len(arg_dict):]:
4311 if arg_name in b:
4312 arg_dict[arg_name] = b[arg_name]
4313
4314 if len(arg_dict) >= len(pos_args):
4315
4316
4317 for arg_name in named_args:
4318 if arg_name in b:
4319 arg_dict[arg_name] = b[arg_name]
4320
4321 return f(**arg_dict)
4322
4323
4324 raise HTTP(404, "Object does not exist")
4325
4328
4330 self.run_procedures = {}
4331 self.csv_procedures = {}
4332 self.xml_procedures = {}
4333 self.rss_procedures = {}
4334 self.json_procedures = {}
4335 self.jsonrpc_procedures = {}
4336 self.jsonrpc2_procedures = {}
4337 self.xmlrpc_procedures = {}
4338 self.amfrpc_procedures = {}
4339 self.amfrpc3_procedures = {}
4340 self.soap_procedures = {}
4341
4343 """
4344 example:
4345
4346 service = Service()
4347 @service.run
4348 def myfunction(a, b):
4349 return a + b
4350 def call():
4351 return service()
4352
4353 Then call it with:
4354
4355 wget http://..../app/default/call/run/myfunction?a=3&b=4
4356
4357 """
4358 self.run_procedures[f.__name__] = f
4359 return f
4360
4362 """
4363 example:
4364
4365 service = Service()
4366 @service.csv
4367 def myfunction(a, b):
4368 return a + b
4369 def call():
4370 return service()
4371
4372 Then call it with:
4373
4374 wget http://..../app/default/call/csv/myfunction?a=3&b=4
4375
4376 """
4377 self.run_procedures[f.__name__] = f
4378 return f
4379
4381 """
4382 example:
4383
4384 service = Service()
4385 @service.xml
4386 def myfunction(a, b):
4387 return a + b
4388 def call():
4389 return service()
4390
4391 Then call it with:
4392
4393 wget http://..../app/default/call/xml/myfunction?a=3&b=4
4394
4395 """
4396 self.run_procedures[f.__name__] = f
4397 return f
4398
4400 """
4401 example:
4402
4403 service = Service()
4404 @service.rss
4405 def myfunction():
4406 return dict(title=..., link=..., description=...,
4407 created_on=..., entries=[dict(title=..., link=...,
4408 description=..., created_on=...])
4409 def call():
4410 return service()
4411
4412 Then call it with:
4413
4414 wget http://..../app/default/call/rss/myfunction
4415
4416 """
4417 self.rss_procedures[f.__name__] = f
4418 return f
4419
4420 - def json(self, f):
4421 """
4422 example:
4423
4424 service = Service()
4425 @service.json
4426 def myfunction(a, b):
4427 return [{a: b}]
4428 def call():
4429 return service()
4430
4431 Then call it with:
4432
4433 wget http://..../app/default/call/json/myfunction?a=hello&b=world
4434
4435 """
4436 self.json_procedures[f.__name__] = f
4437 return f
4438
4440 """
4441 example:
4442
4443 service = Service()
4444 @service.jsonrpc
4445 def myfunction(a, b):
4446 return a + b
4447 def call():
4448 return service()
4449
4450 Then call it with:
4451
4452 wget http://..../app/default/call/jsonrpc/myfunction?a=hello&b=world
4453
4454 """
4455 self.jsonrpc_procedures[f.__name__] = f
4456 return f
4457
4459 """
4460 example:
4461
4462 service = Service()
4463 @service.jsonrpc2
4464 def myfunction(a, b):
4465 return a + b
4466 def call():
4467 return service()
4468
4469 Then call it with:
4470
4471 wget --post-data '{"jsonrpc": "2.0", "id": 1, "method": "myfunction", "params": {"a": 1, "b": 2}}' http://..../app/default/call/jsonrpc2
4472
4473 """
4474 self.jsonrpc2_procedures[f.__name__] = f
4475 return f
4476
4478 """
4479 example:
4480
4481 service = Service()
4482 @service.xmlrpc
4483 def myfunction(a, b):
4484 return a + b
4485 def call():
4486 return service()
4487
4488 The call it with:
4489
4490 wget http://..../app/default/call/xmlrpc/myfunction?a=hello&b=world
4491
4492 """
4493 self.xmlrpc_procedures[f.__name__] = f
4494 return f
4495
4497 """
4498 example:
4499
4500 service = Service()
4501 @service.amfrpc
4502 def myfunction(a, b):
4503 return a + b
4504 def call():
4505 return service()
4506
4507 The call it with:
4508
4509 wget http://..../app/default/call/amfrpc/myfunction?a=hello&b=world
4510
4511 """
4512 self.amfrpc_procedures[f.__name__] = f
4513 return f
4514
4515 - def amfrpc3(self, domain='default'):
4516 """
4517 example:
4518
4519 service = Service()
4520 @service.amfrpc3('domain')
4521 def myfunction(a, b):
4522 return a + b
4523 def call():
4524 return service()
4525
4526 The call it with:
4527
4528 wget http://..../app/default/call/amfrpc3/myfunction?a=hello&b=world
4529
4530 """
4531 if not isinstance(domain, str):
4532 raise SyntaxError("AMF3 requires a domain for function")
4533
4534 def _amfrpc3(f):
4535 if domain:
4536 self.amfrpc3_procedures[domain + '.' + f.__name__] = f
4537 else:
4538 self.amfrpc3_procedures[f.__name__] = f
4539 return f
4540 return _amfrpc3
4541
4542 - def soap(self, name=None, returns=None, args=None, doc=None):
4543 """
4544 example:
4545
4546 service = Service()
4547 @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,})
4548 def myfunction(a, b):
4549 return a + b
4550 def call():
4551 return service()
4552
4553 The call it with:
4554
4555 from gluon.contrib.pysimplesoap.client import SoapClient
4556 client = SoapClient(wsdl="http://..../app/default/call/soap?WSDL")
4557 response = client.MyFunction(a=1,b=2)
4558 return response['result']
4559
4560 Exposes online generated documentation and xml example messages at:
4561 - http://..../app/default/call/soap
4562 """
4563
4564 def _soap(f):
4565 self.soap_procedures[name or f.__name__] = f, returns, args, doc
4566 return f
4567 return _soap
4568
4577
4579 request = current.request
4580 response = current.response
4581 response.headers['Content-Type'] = 'text/x-csv'
4582 if not args:
4583 args = request.args
4584
4585 def none_exception(value):
4586 if isinstance(value, unicode):
4587 return value.encode('utf8')
4588 if hasattr(value, 'isoformat'):
4589 return value.isoformat()[:19].replace('T', ' ')
4590 if value is None:
4591 return '<NULL>'
4592 return value
4593 if args and args[0] in self.run_procedures:
4594 import types
4595 r = universal_caller(self.run_procedures[args[0]],
4596 *args[1:], **dict(request.vars))
4597 s = cStringIO.StringIO()
4598 if hasattr(r, 'export_to_csv_file'):
4599 r.export_to_csv_file(s)
4600 elif r and not isinstance(r, types.GeneratorType) and isinstance(r[0], (dict, Storage)):
4601 import csv
4602 writer = csv.writer(s)
4603 writer.writerow(r[0].keys())
4604 for line in r:
4605 writer.writerow([none_exception(v)
4606 for v in line.values()])
4607 else:
4608 import csv
4609 writer = csv.writer(s)
4610 for line in r:
4611 writer.writerow(line)
4612 return s.getvalue()
4613 self.error()
4614
4628
4641
4655
4658 jrpc_error = Service.jsonrpc_errors.get(code)
4659 if jrpc_error:
4660 self.message, self.description = jrpc_error
4661 self.code, self.info = code, info
4662
4663
4664 jsonrpc_errors = {
4665 -32700: ("Parse error. Invalid JSON was received by the server.", "An error occurred on the server while parsing the JSON text."),
4666 -32600: ("Invalid Request", "The JSON sent is not a valid Request object."),
4667 -32601: ("Method not found", "The method does not exist / is not available."),
4668 -32602: ("Invalid params", "Invalid method parameter(s)."),
4669 -32603: ("Internal error", "Internal JSON-RPC error."),
4670 -32099: ("Server error", "Reserved for implementation-defined server-errors.")}
4671
4672
4674 def return_response(id, result):
4675 return serializers.json({'version': '1.1',
4676 'id': id, 'result': result, 'error': None})
4677
4678 def return_error(id, code, message, data=None):
4679 error = {'name': 'JSONRPCError',
4680 'code': code, 'message': message}
4681 if data is not None:
4682 error['data'] = data
4683 return serializers.json({'id': id,
4684 'version': '1.1',
4685 'error': error,
4686 })
4687
4688 request = current.request
4689 response = current.response
4690 response.headers['Content-Type'] = 'application/json; charset=utf-8'
4691 methods = self.jsonrpc_procedures
4692 data = json_parser.loads(request.body.read())
4693 jsonrpc_2 = data.get('jsonrpc')
4694 if jsonrpc_2:
4695 return self.serve_jsonrpc2(data)
4696 id, method, params = data.get('id'), data.get('method'), data.get('params', [])
4697 if id is None:
4698 return return_error(0, 100, 'missing id')
4699 if not method in methods:
4700 return return_error(id, 100, 'method "%s" does not exist' % method)
4701 try:
4702 if isinstance(params,dict):
4703 s = methods[method](**params)
4704 else:
4705 s = methods[method](*params)
4706 if hasattr(s, 'as_list'):
4707 s = s.as_list()
4708 return return_response(id, s)
4709 except Service.JsonRpcException, e:
4710 return return_error(id, e.code, e.info)
4711 except:
4712 etype, eval, etb = sys.exc_info()
4713 message = '%s: %s' % (etype.__name__, eval)
4714 data = request.is_local and traceback.format_tb(etb)
4715 logger.warning('jsonrpc exception %s\n%s' % (message, traceback.format_tb(etb)))
4716 return return_error(id, 100, message, data)
4717
4719
4720 def return_response(id, result):
4721 if not must_respond:
4722 return None
4723 return serializers.json({'jsonrpc': '2.0',
4724 'id': id, 'result': result})
4725
4726 def return_error(id, code, message=None, data=None):
4727 error = {'code': code}
4728 if Service.jsonrpc_errors.has_key(code):
4729 error['message'] = Service.jsonrpc_errors[code][0]
4730 error['data'] = Service.jsonrpc_errors[code][1]
4731 if message is not None:
4732 error['message'] = message
4733 if data is not None:
4734 error['data'] = data
4735 return serializers.json({'jsonrpc': '2.0',
4736 'id': id,
4737 'error': error})
4738
4739 def validate(data):
4740 """
4741 Validate request as defined in: http://www.jsonrpc.org/specification#request_object.
4742
4743 :param data: The json object.
4744 :type name: str.
4745
4746 :returns:
4747 - True -- if successful
4748 - False -- if no error should be reported (i.e. data is missing 'id' member)
4749
4750 :raises: JsonRPCException
4751
4752 """
4753
4754 iparms = set(data.keys())
4755 mandatory_args = set(['jsonrpc', 'method'])
4756 missing_args = mandatory_args - iparms
4757
4758 if missing_args:
4759 raise Service.JsonRpcException(-32600, 'Missing arguments %s.' % list(missing_args))
4760 if data['jsonrpc'] != '2.0':
4761 raise Service.JsonRpcException(-32603, 'Unsupported jsonrpc version "%s"' % data['jsonrpc'])
4762 if 'id' not in iparms:
4763 return False
4764
4765 return True
4766
4767
4768
4769 request = current.request
4770 response = current.response
4771 if not data:
4772 response.headers['Content-Type'] = 'application/json; charset=utf-8'
4773 try:
4774 data = json_parser.loads(request.body.read())
4775 except ValueError:
4776 return return_error(None, -32700)
4777
4778
4779 if isinstance(data, list) and not batch_element:
4780 retlist = []
4781 for c in data:
4782 retstr = self.serve_jsonrpc2(c, batch_element=True)
4783 if retstr:
4784 retlist.append(retstr)
4785 if len(retlist) == 0:
4786 return ''
4787 else:
4788 return "[" + ','.join(retlist) + "]"
4789 methods = self.jsonrpc2_procedures
4790 methods.update(self.jsonrpc_procedures)
4791
4792 try:
4793 must_respond = validate(data)
4794 except Service.JsonRpcException, e:
4795 return return_error(None, e.code, e.info)
4796
4797 id, method, params = data.get('id'), data['method'], data.get('params', '')
4798 if not method in methods:
4799 return return_error(id, -32601, data='Method "%s" does not exist' % method)
4800 try:
4801 if isinstance(params,dict):
4802 s = methods[method](**params)
4803 else:
4804 s = methods[method](*params)
4805 if hasattr(s, 'as_list'):
4806 s = s.as_list()
4807 if must_respond:
4808 return return_response(id, s)
4809 else:
4810 return ''
4811 except HTTP, e:
4812 raise e
4813 except Service.JsonRpcException, e:
4814 return return_error(id, e.code, e.info)
4815 except:
4816 etype, eval, etb = sys.exc_info()
4817 data = '%s: %s\n' % (etype.__name__, eval) + str(request.is_local and traceback.format_tb(etb))
4818 logger.warning('%s: %s\n%s' % (etype.__name__, eval, traceback.format_tb(etb)))
4819 return return_error(id, -32099, data=data)
4820
4821
4827
4829 try:
4830 import pyamf
4831 import pyamf.remoting.gateway
4832 except:
4833 return "pyamf not installed or not in Python sys.path"
4834 request = current.request
4835 response = current.response
4836 if version == 3:
4837 services = self.amfrpc3_procedures
4838 base_gateway = pyamf.remoting.gateway.BaseGateway(services)
4839 pyamf_request = pyamf.remoting.decode(request.body)
4840 else:
4841 services = self.amfrpc_procedures
4842 base_gateway = pyamf.remoting.gateway.BaseGateway(services)
4843 context = pyamf.get_context(pyamf.AMF0)
4844 pyamf_request = pyamf.remoting.decode(request.body, context)
4845 pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion)
4846 for name, message in pyamf_request:
4847 pyamf_response[name] = base_gateway.getProcessor(message)(message)
4848 response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE
4849 if version == 3:
4850 return pyamf.remoting.encode(pyamf_response).getvalue()
4851 else:
4852 return pyamf.remoting.encode(pyamf_response, context).getvalue()
4853
4855 try:
4856 from gluon.contrib.pysimplesoap.server import SoapDispatcher
4857 except:
4858 return "pysimplesoap not installed in contrib"
4859 request = current.request
4860 response = current.response
4861 procedures = self.soap_procedures
4862
4863 location = "%s://%s%s" % (
4864 request.env.wsgi_url_scheme,
4865 request.env.http_host,
4866 URL(r=request, f="call/soap", vars={}))
4867 namespace = 'namespace' in response and response.namespace or location
4868 documentation = response.description or ''
4869 dispatcher = SoapDispatcher(
4870 name=response.title,
4871 location=location,
4872 action=location,
4873 namespace=namespace,
4874 prefix='pys',
4875 documentation=documentation,
4876 ns=True)
4877 for method, (function, returns, args, doc) in procedures.iteritems():
4878 dispatcher.register_function(method, function, returns, args, doc)
4879 if request.env.request_method == 'POST':
4880 fault = {}
4881
4882 response.headers['Content-Type'] = 'text/xml'
4883 xml = dispatcher.dispatch(request.body.read(), fault=fault)
4884 if fault:
4885
4886 response.status = 500
4887
4888 return xml
4889 elif 'WSDL' in request.vars:
4890
4891 response.headers['Content-Type'] = 'text/xml'
4892 return dispatcher.wsdl()
4893 elif 'op' in request.vars:
4894
4895 response.headers['Content-Type'] = 'text/html'
4896 method = request.vars['op']
4897 sample_req_xml, sample_res_xml, doc = dispatcher.help(method)
4898 body = [H1("Welcome to Web2Py SOAP webservice gateway"),
4899 A("See all webservice operations",
4900 _href=URL(r=request, f="call/soap", vars={})),
4901 H2(method),
4902 P(doc),
4903 UL(LI("Location: %s" % dispatcher.location),
4904 LI("Namespace: %s" % dispatcher.namespace),
4905 LI("SoapAction: %s" % dispatcher.action),
4906 ),
4907 H3("Sample SOAP XML Request Message:"),
4908 CODE(sample_req_xml, language="xml"),
4909 H3("Sample SOAP XML Response Message:"),
4910 CODE(sample_res_xml, language="xml"),
4911 ]
4912 return {'body': body}
4913 else:
4914
4915 response.headers['Content-Type'] = 'text/html'
4916 body = [H1("Welcome to Web2Py SOAP webservice gateway"),
4917 P(response.description),
4918 P("The following operations are available"),
4919 A("See WSDL for webservice description",
4920 _href=URL(r=request, f="call/soap", vars={"WSDL":None})),
4921 UL([LI(A("%s: %s" % (method, doc or ''),
4922 _href=URL(r=request, f="call/soap", vars={'op': method})))
4923 for method, doc in dispatcher.list_methods()]),
4924 ]
4925 return {'body': body}
4926
4928 """
4929 register services with:
4930 service = Service()
4931 @service.run
4932 @service.rss
4933 @service.json
4934 @service.jsonrpc
4935 @service.xmlrpc
4936 @service.amfrpc
4937 @service.amfrpc3('domain')
4938 @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,})
4939
4940 expose services with
4941
4942 def call(): return service()
4943
4944 call services with
4945 http://..../app/default/call/run?[parameters]
4946 http://..../app/default/call/rss?[parameters]
4947 http://..../app/default/call/json?[parameters]
4948 http://..../app/default/call/jsonrpc
4949 http://..../app/default/call/xmlrpc
4950 http://..../app/default/call/amfrpc
4951 http://..../app/default/call/amfrpc3
4952 http://..../app/default/call/soap
4953 """
4954
4955 request = current.request
4956 if len(request.args) < 1:
4957 raise HTTP(404, "Not Found")
4958 arg0 = request.args(0)
4959 if arg0 == 'run':
4960 return self.serve_run(request.args[1:])
4961 elif arg0 == 'rss':
4962 return self.serve_rss(request.args[1:])
4963 elif arg0 == 'csv':
4964 return self.serve_csv(request.args[1:])
4965 elif arg0 == 'xml':
4966 return self.serve_xml(request.args[1:])
4967 elif arg0 == 'json':
4968 return self.serve_json(request.args[1:])
4969 elif arg0 == 'jsonrpc':
4970 return self.serve_jsonrpc()
4971 elif arg0 == 'jsonrpc2':
4972 return self.serve_jsonrpc2()
4973 elif arg0 == 'xmlrpc':
4974 return self.serve_xmlrpc()
4975 elif arg0 == 'amfrpc':
4976 return self.serve_amfrpc()
4977 elif arg0 == 'amfrpc3':
4978 return self.serve_amfrpc(3)
4979 elif arg0 == 'soap':
4980 return self.serve_soap()
4981 else:
4982 self.error()
4983
4985 raise HTTP(404, "Object does not exist")
4986
4989 """
4990 Executes a task on completion of the called action. For example:
4991
4992 from gluon.tools import completion
4993 @completion(lambda d: logging.info(repr(d)))
4994 def index():
4995 return dict(message='hello')
4996
4997 It logs the output of the function every time input is called.
4998 The argument of completion is executed in a new thread.
4999 """
5000 def _completion(f):
5001 def __completion(*a, **b):
5002 d = None
5003 try:
5004 d = f(*a, **b)
5005 return d
5006 finally:
5007 thread.start_new_thread(callback, (d,))
5008 return __completion
5009 return _completion
5010
5013 if isinstance(d, datetime.datetime):
5014 dt = datetime.datetime.now() - d
5015 elif isinstance(d, datetime.date):
5016 dt = datetime.date.today() - d
5017 elif not d:
5018 return ''
5019 else:
5020 return '[invalid date]'
5021 if dt.days < 0:
5022 suffix = ' from now'
5023 dt = -dt
5024 else:
5025 suffix = ' ago'
5026 if dt.days >= 2 * 365:
5027 return T('%d years' + suffix) % int(dt.days / 365)
5028 elif dt.days >= 365:
5029 return T('1 year' + suffix)
5030 elif dt.days >= 60:
5031 return T('%d months' + suffix) % int(dt.days / 30)
5032 elif dt.days > 21:
5033 return T('1 month' + suffix)
5034 elif dt.days >= 14:
5035 return T('%d weeks' + suffix) % int(dt.days / 7)
5036 elif dt.days >= 7:
5037 return T('1 week' + suffix)
5038 elif dt.days > 1:
5039 return T('%d days' + suffix) % dt.days
5040 elif dt.days == 1:
5041 return T('1 day' + suffix)
5042 elif dt.seconds >= 2 * 60 * 60:
5043 return T('%d hours' + suffix) % int(dt.seconds / 3600)
5044 elif dt.seconds >= 60 * 60:
5045 return T('1 hour' + suffix)
5046 elif dt.seconds >= 2 * 60:
5047 return T('%d minutes' + suffix) % int(dt.seconds / 60)
5048 elif dt.seconds >= 60:
5049 return T('1 minute' + suffix)
5050 elif dt.seconds > 1:
5051 return T('%d seconds' + suffix) % dt.seconds
5052 elif dt.seconds == 1:
5053 return T('1 second' + suffix)
5054 else:
5055 return T('now')
5056
5066 lock1 = thread.allocate_lock()
5067 lock2 = thread.allocate_lock()
5068 lock1.acquire()
5069 thread.start_new_thread(f, ())
5070 a = PluginManager()
5071 a.x = 5
5072 lock1.release()
5073 lock2.acquire()
5074 return a.x
5075
5078 """
5079
5080 Plugin Manager is similar to a storage object but it is a single level singleton
5081 this means that multiple instances within the same thread share the same attributes
5082 Its constructor is also special. The first argument is the name of the plugin you are defining.
5083 The named arguments are parameters needed by the plugin with default values.
5084 If the parameters were previous defined, the old values are used.
5085
5086 For example:
5087
5088 ### in some general configuration file:
5089 >>> plugins = PluginManager()
5090 >>> plugins.me.param1=3
5091
5092 ### within the plugin model
5093 >>> _ = PluginManager('me',param1=5,param2=6,param3=7)
5094
5095 ### where the plugin is used
5096 >>> print plugins.me.param1
5097 3
5098 >>> print plugins.me.param2
5099 6
5100 >>> plugins.me.param3 = 8
5101 >>> print plugins.me.param3
5102 8
5103
5104 Here are some tests:
5105
5106 >>> a=PluginManager()
5107 >>> a.x=6
5108 >>> b=PluginManager('check')
5109 >>> print b.x
5110 6
5111 >>> b=PluginManager() # reset settings
5112 >>> print b.x
5113 <Storage {}>
5114 >>> b.x=7
5115 >>> print a.x
5116 7
5117 >>> a.y.z=8
5118 >>> print b.y.z
5119 8
5120 >>> test_thread_separation()
5121 5
5122 >>> plugins=PluginManager('me',db='mydb')
5123 >>> print plugins.me.db
5124 mydb
5125 >>> print 'me' in plugins
5126 True
5127 >>> print plugins.me.installed
5128 True
5129 """
5130 instances = {}
5131
5133 id = thread.get_ident()
5134 lock = thread.allocate_lock()
5135 try:
5136 lock.acquire()
5137 try:
5138 return cls.instances[id]
5139 except KeyError:
5140 instance = object.__new__(cls, *a, **b)
5141 cls.instances[id] = instance
5142 return instance
5143 finally:
5144 lock.release()
5145
5146 - def __init__(self, plugin=None, **defaults):
5153
5155 if not key in self.__dict__:
5156 self.__dict__[key] = Storage()
5157 return self.__dict__[key]
5158
5160 return self.__dict__.keys()
5161
5163 return key in self.__dict__
5164
5167 - def __init__(self, base=None, basename=None, extensions=None, allow_download=True):
5168 """
5169 Usage:
5170
5171 def static():
5172 return dict(files=Expose())
5173
5174 or
5175
5176 def static():
5177 path = os.path.join(request.folder,'static','public')
5178 return dict(files=Expose(path,basename='public'))
5179
5180 extensions:
5181 an optional list of file extensions for filtering displayed files:
5182 ['.py', '.jpg']
5183 allow_download: whether to allow downloading selected files
5184 """
5185 current.session.forget()
5186 base = base or os.path.join(current.request.folder, 'static')
5187 basename = basename or current.request.function
5188 self.basename = basename
5189 self.args = current.request.raw_args and \
5190 [arg for arg in current.request.raw_args.split('/') if arg] or []
5191 filename = os.path.join(base, *self.args)
5192 if not os.path.exists(filename):
5193 raise HTTP(404, "FILE NOT FOUND")
5194 if not os.path.normpath(filename).startswith(base):
5195 raise HTTP(401, "NOT AUTHORIZED")
5196 if allow_download and not os.path.isdir(filename):
5197 current.response.headers['Content-Type'] = contenttype(filename)
5198 raise HTTP(200, open(filename, 'rb'), **current.response.headers)
5199 self.path = path = os.path.join(filename, '*')
5200 self.folders = [f[len(path) - 1:] for f in sorted(glob.glob(path))
5201 if os.path.isdir(f) and not self.isprivate(f)]
5202 self.filenames = [f[len(path) - 1:] for f in sorted(glob.glob(path))
5203 if not os.path.isdir(f) and not self.isprivate(f)]
5204 if 'README' in self.filenames:
5205 readme = open(os.path.join(filename,'README')).read()
5206 self.paragraph = MARKMIN(readme)
5207 else:
5208 self.paragraph = None
5209 if extensions:
5210 self.filenames = [f for f in self.filenames
5211 if os.path.splitext(f)[-1] in extensions]
5212
5222
5230
5231 @staticmethod
5234
5235 @staticmethod
5237 return os.path.splitext(f)[-1].lower() in (
5238 '.png', '.jpg', '.jpeg', '.gif', '.tiff')
5239
5241 if self.filenames:
5242 return SPAN(H3('Files'),
5243 TABLE(*[TR(TD(A(f, _href=URL(args=self.args + [f]))),
5244 TD(IMG(_src=URL(args=self.args + [f]),
5245 _style='max-width:%spx' % width)
5246 if width and self.isimage(f) else ''))
5247 for f in self.filenames],
5248 **dict(_class="table")))
5249 return ''
5250
5257
5258
5259 -class Wiki(object):
5260 everybody = 'everybody'
5261 rows_page = 25
5263 return MARKMIN(body, extra=self.settings.extra,
5264 url=True, environment=self.env,
5265 autolinks=lambda link: expand_one(link, {})).xml()
5266
5272
5275
5286
5287 @staticmethod
5289 """
5290 In wiki docs allows @{component:controller/function/args}
5291 which renders as a LOAD(..., ajax=True)
5292 """
5293 items = text.split('/')
5294 controller, function, args = items[0], items[1], items[2:]
5295 return LOAD(controller, function, args=args, ajax=True).xml()
5296
5312 r = custom_render
5313 else:
5314 raise ValueError(
5315 "Invalid render type %s" % type(self.settings.render))
5316 return r
5317
5318 - def __init__(self, auth, env=None, render='markmin',
5319 manage_permissions=False, force_prefix='',
5320 restrict_search=False, extra=None,
5321 menu_groups=None, templates=None, migrate=True,
5322 controller=None, function=None, groups=None):
5323
5324 settings = self.settings = auth.settings.wiki
5325
5326 """render argument options:
5327 - "markmin"
5328 - "html"
5329 - <function>
5330 Sets a custom render function
5331 - dict(html=<function>, markmin=...):
5332 dict(...) allows multiple custom render functions
5333 - "multiple"
5334 Is the same as {}. It enables per-record formats
5335 using builtins
5336 """
5337 engines = set(['markmin', 'html'])
5338 show_engine = False
5339 if render == "multiple":
5340 render = {}
5341 if isinstance(render, dict):
5342 [engines.add(key) for key in render]
5343 show_engine = True
5344 settings.render = render
5345 perms = settings.manage_permissions = manage_permissions
5346
5347 settings.force_prefix = force_prefix
5348 settings.restrict_search = restrict_search
5349 settings.extra = extra or {}
5350 settings.menu_groups = menu_groups
5351 settings.templates = templates
5352 settings.controller = controller
5353 settings.function = function
5354 settings.groups = auth.user_groups.values() \
5355 if groups is None else groups
5356
5357 db = auth.db
5358 self.env = env or {}
5359 self.env['component'] = Wiki.component
5360 self.auth = auth
5361 self.wiki_menu_items = None
5362
5363 if self.auth.user:
5364 self.settings.force_prefix = force_prefix % self.auth.user
5365 else:
5366 self.settings.force_prefix = force_prefix
5367
5368 self.host = current.request.env.http_host
5369
5370 table_definitions = [
5371 ('wiki_page', {
5372 'args':[
5373 Field('slug',
5374 requires=[IS_SLUG(),
5375 IS_NOT_IN_DB(db, 'wiki_page.slug')],
5376 writable=False),
5377 Field('title', length=255, unique=True),
5378 Field('body', 'text', notnull=True),
5379 Field('tags', 'list:string'),
5380 Field('can_read', 'list:string',
5381 writable=perms,
5382 readable=perms,
5383 default=[Wiki.everybody]),
5384 Field('can_edit', 'list:string',
5385 writable=perms, readable=perms,
5386 default=[Wiki.everybody]),
5387 Field('changelog'),
5388 Field('html', 'text',
5389 compute=self.get_renderer(),
5390 readable=False, writable=False),
5391 Field('render', default="markmin",
5392 readable=show_engine,
5393 writable=show_engine,
5394 requires=IS_EMPTY_OR(
5395 IS_IN_SET(engines))),
5396 auth.signature],
5397 'vars':{'format':'%(title)s', 'migrate':migrate}}),
5398 ('wiki_tag', {
5399 'args':[
5400 Field('name'),
5401 Field('wiki_page', 'reference wiki_page'),
5402 auth.signature],
5403 'vars':{'format':'%(title)s', 'migrate':migrate}}),
5404 ('wiki_media', {
5405 'args':[
5406 Field('wiki_page', 'reference wiki_page'),
5407 Field('title', required=True),
5408 Field('filename', 'upload', required=True),
5409 auth.signature],
5410 'vars':{'format':'%(title)s', 'migrate':migrate}}),
5411 ]
5412
5413
5414 for key, value in table_definitions:
5415 args = []
5416 if not key in db.tables():
5417
5418 extra_fields = auth.settings.extra_fields
5419 if extra_fields:
5420 if key in extra_fields:
5421 if extra_fields[key]:
5422 for field in extra_fields[key]:
5423 args.append(field)
5424 args += value['args']
5425 db.define_table(key, *args, **value['vars'])
5426
5427 if self.settings.templates is None and not \
5428 self.settings.manage_permissions:
5429 self.settings.templates = db.wiki_page.tags.contains('template')&\
5430 db.wiki_page.can_read.contains('everybody')
5431
5432 def update_tags_insert(page, id, db=db):
5433 for tag in page.tags or []:
5434 tag = tag.strip().lower()
5435 if tag:
5436 db.wiki_tag.insert(name=tag, wiki_page=id)
5437
5438 def update_tags_update(dbset, page, db=db):
5439 page = dbset.select(limitby=(0,1)).first()
5440 db(db.wiki_tag.wiki_page == page.id).delete()
5441 for tag in page.tags or []:
5442 tag = tag.strip().lower()
5443 if tag:
5444 db.wiki_tag.insert(name=tag, wiki_page=page.id)
5445 db.wiki_page._after_insert.append(update_tags_insert)
5446 db.wiki_page._after_update.append(update_tags_update)
5447
5448 if (auth.user and
5449 check_credentials(current.request, gae_login=False) and
5450 not 'wiki_editor' in auth.user_groups.values() and
5451 self.settings.groups == auth.user_groups.values()):
5452 group = db.auth_group(role='wiki_editor')
5453 gid = group.id if group else db.auth_group.insert(
5454 role='wiki_editor')
5455 auth.add_membership(gid)
5456
5457 settings.lock_keys = True
5458
5459
5460
5463
5475
5485
5491
5494
5504
5505
5506
5508 """adds the menu if not present"""
5509 if (not self.wiki_menu_items and
5510 self.settings.controller and
5511 self.settings.function):
5512 self.wiki_menu_items = self.menu(self.settings.controller,
5513 self.settings.function)
5514 current.response.menu += self.wiki_menu_items
5515
5551
5553 if not self.can_read(page):
5554 mm = (page.body or '').replace('\r', '')
5555 ps = [p for p in mm.split('\n\n')
5556 if not p.startswith('#') and p.strip()]
5557 if ps:
5558 return ps[0]
5559 return ''
5560
5562 return (body or '').replace('://HOSTNAME', '://%s' % self.host)
5563
5564 - def read(self, slug, force_render=False):
5565 if slug in '_cloud':
5566 return self.cloud()
5567 elif slug in '_search':
5568 return self.search()
5569 page = self.auth.db.wiki_page(slug=slug)
5570 if page and (not self.can_read(page)):
5571 return self.not_authorized(page)
5572 if current.request.extension == 'html':
5573 if not page:
5574 url = URL(args=('_create', slug))
5575 return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn"))
5576 else:
5577 html = page.html if not force_render else self.get_renderer()(page)
5578 content = XML(self.fix_hostname(html))
5579 return dict(title=page.title,
5580 slug=page.slug,
5581 page=page,
5582 content=content,
5583 tags=page.tags,
5584 created_on=page.created_on,
5585 modified_on=page.modified_on)
5586 elif current.request.extension == 'load':
5587 return self.fix_hostname(page.html) if page else ''
5588 else:
5589 if not page:
5590 raise HTTP(404)
5591 else:
5592 return dict(title=page.title,
5593 slug=page.slug,
5594 page=page,
5595 content=page.body,
5596 tags=page.tags,
5597 created_on=page.created_on,
5598 modified_on=page.modified_on)
5599
5600 - def edit(self,slug,from_template=0):
5601 auth = self.auth
5602 db = auth.db
5603 page = db.wiki_page(slug=slug)
5604 if not self.can_edit(page):
5605 return self.not_authorized(page)
5606 title_guess = ' '.join(c.capitalize() for c in slug.split('-'))
5607 if not page:
5608 if not (self.can_manage() or
5609 slug.startswith(self.settings.force_prefix)):
5610 current.session.flash = 'slug must have "%s" prefix' \
5611 % self.settings.force_prefix
5612 redirect(URL(args=('_create')))
5613 db.wiki_page.can_read.default = [Wiki.everybody]
5614 db.wiki_page.can_edit.default = [auth.user_group_role()]
5615 db.wiki_page.title.default = title_guess
5616 db.wiki_page.slug.default = slug
5617 if slug == 'wiki-menu':
5618 db.wiki_page.body.default = \
5619 '- Menu Item > @////index\n- - Submenu > http://web2py.com'
5620 else:
5621 db.wiki_page.body.default = db(db.wiki_page.id==from_template).select(db.wiki_page.body)[0].body if int(from_template) > 0 else '## %s\n\npage content' % title_guess
5622 vars = current.request.post_vars
5623 if vars.body:
5624 vars.body = vars.body.replace('://%s' % self.host, '://HOSTNAME')
5625 form = SQLFORM(db.wiki_page, page, deletable=True,
5626 formstyle='table2cols', showid=False).process()
5627 if form.deleted:
5628 current.session.flash = 'page deleted'
5629 redirect(URL())
5630 elif form.accepted:
5631 current.session.flash = 'page created'
5632 redirect(URL(args=slug))
5633 script = """
5634 jQuery(function() {
5635 if (!jQuery('#wiki_page_body').length) return;
5636 var pagecontent = jQuery('#wiki_page_body');
5637 pagecontent.css('font-family',
5638 'Monaco,Menlo,Consolas,"Courier New",monospace');
5639 var prevbutton = jQuery('<button class="btn nopreview">Preview</button>');
5640 var preview = jQuery('<div id="preview"></div>').hide();
5641 var previewmedia = jQuery('<div id="previewmedia"></div>');
5642 var form = pagecontent.closest('form');
5643 preview.insertBefore(form);
5644 prevbutton.insertBefore(form);
5645 if(%(link_media)s) {
5646 var mediabutton = jQuery('<button class="btn nopreview">Media</button>');
5647 mediabutton.insertBefore(form);
5648 previewmedia.insertBefore(form);
5649 mediabutton.toggle(function() {
5650 web2py_component('%(urlmedia)s', 'previewmedia');
5651 }, function() {
5652 previewmedia.empty();
5653 });
5654 }
5655 prevbutton.click(function(e) {
5656 e.preventDefault();
5657 if (prevbutton.hasClass('nopreview')) {
5658 prevbutton.addClass('preview').removeClass(
5659 'nopreview').html('Edit Source');
5660 try{var wiki_render = jQuery('#wiki_page_render').val()}
5661 catch(e){var wiki_render = null;}
5662 web2py_ajax_page('post', \
5663 '%(url)s', {body: jQuery('#wiki_page_body').val(), \
5664 render: wiki_render}, 'preview');
5665 form.fadeOut('fast', function() {preview.fadeIn()});
5666 } else {
5667 prevbutton.addClass(
5668 'nopreview').removeClass('preview').html('Preview');
5669 preview.fadeOut('fast', function() {form.fadeIn()});
5670 }
5671 })
5672 })
5673 """ % dict(url=URL(args=('_preview', slug)),link_media=('true' if page else 'false'),
5674 urlmedia=URL(extension='load',
5675 args=('_editmedia',slug),
5676 vars=dict(embedded=1)))
5677 return dict(content=TAG[''](form, SCRIPT(script)))
5678
5713
5715 if not self.can_edit():
5716 return self.not_authorized()
5717 db = self.auth.db
5718 slugs=db(db.wiki_page.id>0).select(db.wiki_page.id,db.wiki_page.slug)
5719 options=[OPTION(row.slug,_value=row.id) for row in slugs]
5720 options.insert(0, OPTION('',_value=''))
5721 fields = [Field("slug", default=current.request.args(1) or
5722 self.settings.force_prefix,
5723 requires=(IS_SLUG(), IS_NOT_IN_DB(db,db.wiki_page.slug))),]
5724 if self.settings.templates:
5725 fields.append(
5726 Field("from_template", "reference wiki_page",
5727 requires=IS_EMPTY_OR(
5728 IS_IN_DB(db(self.settings.templates),
5729 db.wiki_page._id,
5730 '%(slug)s')),
5731 comment=current.T(
5732 "Choose Template or empty for new Page")))
5733 form = SQLFORM.factory(*fields, **dict(_class="well"))
5734 form.element("[type=submit]").attributes["_value"] = \
5735 current.T("Create Page from Slug")
5736
5737 if form.process().accepted:
5738 form.vars.from_template = 0 if not form.vars.from_template \
5739 else form.vars.from_template
5740 redirect(URL(args=('_edit', form.vars.slug,form.vars.from_template or 0)))
5741 return dict(content=form)
5742
5744 if not self.can_manage():
5745 return self.not_authorized()
5746 self.auth.db.wiki_page.slug.represent = lambda slug, row: SPAN(
5747 '@////%s' % slug)
5748 self.auth.db.wiki_page.title.represent = lambda title, row: \
5749 A(title, _href=URL(args=row.slug))
5750 wiki_table = self.auth.db.wiki_page
5751 content = SQLFORM.grid(
5752 wiki_table,
5753 fields = [wiki_table.slug,
5754 wiki_table.title, wiki_table.tags,
5755 wiki_table.can_read, wiki_table.can_edit],
5756 links=[
5757 lambda row:
5758 A('edit', _href=URL(args=('_edit', row.slug)),_class='btn'),
5759 lambda row:
5760 A('media', _href=URL(args=('_editmedia', row.slug)),_class='btn')],
5761 details=False, editable=False, deletable=False, create=False,
5762 orderby=self.auth.db.wiki_page.title,
5763 args=['_pages'],
5764 user_signature=False)
5765
5766 return dict(content=content)
5767
5788
5790 db = self.auth.db
5791 request = current.request
5792 menu_page = db.wiki_page(slug='wiki-menu')
5793 menu = []
5794 if menu_page:
5795 tree = {'': menu}
5796 regex = re.compile('[\r\n\t]*(?P<base>(\s*\-\s*)+)(?P<title>\w.*?)\s+\>\s+(?P<link>\S+)')
5797 for match in regex.finditer(self.fix_hostname(menu_page.body)):
5798 base = match.group('base').replace(' ', '')
5799 title = match.group('title')
5800 link = match.group('link')
5801 title_page = None
5802 if link.startswith('@'):
5803 items = link[2:].split('/')
5804 if len(items) > 3:
5805 title_page = items[3]
5806 link = URL(a=items[0] or None, c=items[1] or controller,
5807 f=items[2] or function, args=items[3:])
5808 parent = tree.get(base[1:], tree[''])
5809 subtree = []
5810 tree[base] = subtree
5811 parent.append((current.T(title),
5812 request.args(0) == title_page,
5813 link, subtree))
5814 if self.can_see_menu():
5815 submenu = []
5816 menu.append((current.T('[Wiki]'), None, None, submenu))
5817 if URL() == URL(controller, function):
5818 if not str(request.args(0)).startswith('_'):
5819 slug = request.args(0) or 'index'
5820 mode = 1
5821 elif request.args(0) == '_edit':
5822 slug = request.args(1) or 'index'
5823 mode = 2
5824 elif request.args(0) == '_editmedia':
5825 slug = request.args(1) or 'index'
5826 mode = 3
5827 else:
5828 mode = 0
5829 if mode in (2, 3):
5830 submenu.append((current.T('View Page'), None,
5831 URL(controller, function, args=slug)))
5832 if mode in (1, 3):
5833 submenu.append((current.T('Edit Page'), None,
5834 URL(controller, function, args=('_edit', slug))))
5835 if mode in (1, 2):
5836 submenu.append((current.T('Edit Page Media'), None,
5837 URL(controller, function, args=('_editmedia', slug))))
5838
5839 submenu.append((current.T('Create New Page'), None,
5840 URL(controller, function, args=('_create'))))
5841
5842 if self.can_manage():
5843 submenu.append((current.T('Manage Pages'), None,
5844 URL(controller, function, args=('_pages'))))
5845 submenu.append((current.T('Edit Menu'), None,
5846 URL(controller, function, args=('_edit', 'wiki-menu'))))
5847
5848 submenu.append((current.T('Search Pages'), None,
5849 URL(controller, function, args=('_search'))))
5850 return menu
5851
5852 - def search(self, tags=None, query=None, cloud=True, preview=True,
5853 limitby=(0, 100), orderby=None):
5854 if not self.can_search():
5855 return self.not_authorized()
5856 request = current.request
5857 content = CAT()
5858 if tags is None and query is None:
5859 form = FORM(INPUT(_name='q', requires=IS_NOT_EMPTY(),
5860 value=request.vars.q),
5861 INPUT(_type="submit", _value=current.T('Search')),
5862 _method='GET')
5863 content.append(DIV(form, _class='w2p_wiki_form'))
5864 if request.vars.q:
5865 tags = [v.strip() for v in request.vars.q.split(',')]
5866 tags = [v.lower() for v in tags if v]
5867 if tags or not query is None:
5868 db = self.auth.db
5869 count = db.wiki_tag.wiki_page.count()
5870 fields = [db.wiki_page.id, db.wiki_page.slug,
5871 db.wiki_page.title, db.wiki_page.tags,
5872 db.wiki_page.can_read]
5873 if preview:
5874 fields.append(db.wiki_page.body)
5875 if query is None:
5876 query = (db.wiki_page.id == db.wiki_tag.wiki_page) &\
5877 (db.wiki_tag.name.belongs(tags))
5878 query = query | db.wiki_page.title.contains(request.vars.q)
5879 if self.settings.restrict_search and not self.manage():
5880 query = query & (db.wiki_page.created_by == self.auth.user_id)
5881 pages = db(query).select(count,
5882 *fields, **dict(orderby=orderby or ~count,
5883 groupby=reduce(lambda a, b: a | b, fields),
5884 distinct=True,
5885 limitby=limitby))
5886 if request.extension in ('html', 'load'):
5887 if not pages:
5888 content.append(DIV(current.T("No results"),
5889 _class='w2p_wiki_form'))
5890
5891 def link(t):
5892 return A(t, _href=URL(args='_search', vars=dict(q=t)))
5893 items = [DIV(H3(A(p.wiki_page.title, _href=URL(
5894 args=p.wiki_page.slug))),
5895 MARKMIN(self.first_paragraph(p.wiki_page))
5896 if preview else '',
5897 DIV(_class='w2p_wiki_tags',
5898 *[link(t.strip()) for t in
5899 p.wiki_page.tags or [] if t.strip()]),
5900 _class='w2p_wiki_search_item')
5901 for p in pages]
5902 content.append(DIV(_class='w2p_wiki_pages', *items))
5903 else:
5904 cloud = False
5905 content = [p.wiki_page.as_dict() for p in pages]
5906 elif cloud:
5907 content.append(self.cloud()['content'])
5908 if request.extension == 'load':
5909 return content
5910 return dict(content=content)
5911
5913 db = self.auth.db
5914 count = db.wiki_tag.wiki_page.count(distinct=True)
5915 ids = db(db.wiki_tag).select(
5916 db.wiki_tag.name, count,
5917 distinct=True,
5918 groupby=db.wiki_tag.name,
5919 orderby=~count, limitby=(0, 20))
5920 if ids:
5921 a, b = ids[0](count), ids[-1](count)
5922
5923 def style(c):
5924 STYLE = 'padding:0 0.2em;line-height:%.2fem;font-size:%.2fem'
5925 size = (1.5 * (c - b) / max(a - b, 1) + 1.3)
5926 return STYLE % (1.3, size)
5927 items = []
5928 for item in ids:
5929 items.append(A(item.wiki_tag.name,
5930 _style=style(item(count)),
5931 _href=URL(args='_search',
5932 vars=dict(q=item.wiki_tag.name))))
5933 items.append(' ')
5934 return dict(content=DIV(_class='w2p_cloud', *items))
5935
5943
5945 - def __init__(
5946 self,
5947 filename,
5948 section,
5949 default_values={}
5950 ):
5951 self.config = ConfigParser.ConfigParser(default_values)
5952 self.config.read(filename)
5953 if not self.config.has_section(section):
5954 self.config.add_section(section)
5955 self.section = section
5956 self.filename = filename
5957
5964
5965 - def save(self, options):
5966 for option, value in options:
5967 self.config.set(self.section, option, value)
5968 try:
5969 self.config.write(open(self.filename, 'w'))
5970 result = True
5971 except:
5972 current.session['settings_%s' % self.section] = dict(self.config.items(self.section))
5973 result = False
5974 return result
5975
5976 if __name__ == '__main__':
5977 import doctest
5978 doctest.testmod()
5979