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 from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
26
27 from gluon.contenttype import contenttype
28 from gluon.storage import Storage, StorageList, Settings, Messages
29 from gluon.utils import web2py_uuid
30 from gluon.fileutils import read_file, check_credentials
31 from gluon import *
32 from gluon.contrib.autolinks import expand_one
33 from gluon.contrib.markmin.markmin2html import \
34 replace_at_urls, replace_autolinks, replace_components
35 from gluon.dal import Row, Set, Query
36
37 import gluon.serializers as serializers
38
39 try:
40
41 import json as json_parser
42 except ImportError:
43 try:
44
45 import simplejson as json_parser
46 except:
47
48 import contrib.simplejson as json_parser
49
50 __all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki',
51 'PluginManager', 'fetch', 'geocode', 'prettydate']
52
53
54 logger = logging.getLogger("web2py")
55
56 DEFAULT = lambda: None
57
58
59 -def getarg(position, default=None):
60 args = current.request.args
61 if position < 0 and len(args) >= -position:
62 return args[position]
63 elif position >= 0 and len(args) > position:
64 return args[position]
65 else:
66 return default
67
68
69 -def callback(actions, form, tablename=None):
70 if actions:
71 if tablename and isinstance(actions, dict):
72 actions = actions.get(tablename, [])
73 if not isinstance(actions, (list, tuple)):
74 actions = [actions]
75 [action(form) for action in actions]
76
79 b = []
80 for item in a:
81 if isinstance(item, (list, tuple)):
82 b = b + list(item)
83 else:
84 b.append(item)
85 return b
86
93
101
102
103 -class Mail(object):
104 """
105 Class for configuring and sending emails with alternative text / html
106 body, multiple attachments and encryption support
107
108 Works with SMTP and Google App Engine.
109 """
110
112 """
113 Email attachment
114
115 Arguments:
116
117 payload: path to file or file-like object with read() method
118 filename: name of the attachment stored in message; if set to
119 None, it will be fetched from payload path; file-like
120 object payload must have explicit filename specified
121 content_id: id of the attachment; automatically contained within
122 < and >
123 content_type: content type of the attachment; if set to None,
124 it will be fetched from filename using gluon.contenttype
125 module
126 encoding: encoding of all strings passed to this function (except
127 attachment body)
128
129 Content ID is used to identify attachments within the html body;
130 in example, attached image with content ID 'photo' may be used in
131 html message as a source of img tag <img src="cid:photo" />.
132
133 Examples:
134
135 #Create attachment from text file:
136 attachment = Mail.Attachment('/path/to/file.txt')
137
138 Content-Type: text/plain
139 MIME-Version: 1.0
140 Content-Disposition: attachment; filename="file.txt"
141 Content-Transfer-Encoding: base64
142
143 SOMEBASE64CONTENT=
144
145 #Create attachment from image file with custom filename and cid:
146 attachment = Mail.Attachment('/path/to/file.png',
147 filename='photo.png',
148 content_id='photo')
149
150 Content-Type: image/png
151 MIME-Version: 1.0
152 Content-Disposition: attachment; filename="photo.png"
153 Content-Id: <photo>
154 Content-Transfer-Encoding: base64
155
156 SOMEOTHERBASE64CONTENT=
157 """
158
159 - def __init__(
160 self,
161 payload,
162 filename=None,
163 content_id=None,
164 content_type=None,
165 encoding='utf-8'):
166 if isinstance(payload, str):
167 if filename is None:
168 filename = os.path.basename(payload)
169 payload = read_file(payload, 'rb')
170 else:
171 if filename is None:
172 raise Exception('Missing attachment name')
173 payload = payload.read()
174 filename = filename.encode(encoding)
175 if content_type is None:
176 content_type = contenttype(filename)
177 self.my_filename = filename
178 self.my_payload = payload
179 MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1))
180 self.set_payload(payload)
181 self['Content-Disposition'] = 'attachment; filename="%s"' % filename
182 if not content_id is None:
183 self['Content-Id'] = '<%s>' % content_id.encode(encoding)
184 Encoders.encode_base64(self)
185
186 - def __init__(self, server=None, sender=None, login=None, tls=True):
187 """
188 Main Mail object
189
190 Arguments:
191
192 server: SMTP server address in address:port notation
193 sender: sender email address
194 login: sender login name and password in login:password notation
195 or None if no authentication is required
196 tls: enables/disables encryption (True by default)
197
198 In Google App Engine use:
199
200 server='gae'
201
202 For sake of backward compatibility all fields are optional and default
203 to None, however, to be able to send emails at least server and sender
204 must be specified. They are available under following fields:
205
206 mail.settings.server
207 mail.settings.sender
208 mail.settings.login
209
210 When server is 'logging', email is logged but not sent (debug mode)
211
212 Optionally you can use PGP encryption or X509:
213
214 mail.settings.cipher_type = None
215 mail.settings.gpg_home = None
216 mail.settings.sign = True
217 mail.settings.sign_passphrase = None
218 mail.settings.encrypt = True
219 mail.settings.x509_sign_keyfile = None
220 mail.settings.x509_sign_certfile = None
221 mail.settings.x509_nocerts = False
222 mail.settings.x509_crypt_certfiles = None
223
224 cipher_type : None
225 gpg - need a python-pyme package and gpgme lib
226 x509 - smime
227 gpg_home : you can set a GNUPGHOME environment variable
228 to specify home of gnupg
229 sign : sign the message (True or False)
230 sign_passphrase : passphrase for key signing
231 encrypt : encrypt the message
232 ... x509 only ...
233 x509_sign_keyfile : the signers private key filename (PEM format)
234 x509_sign_certfile: the signers certificate filename (PEM format)
235 x509_nocerts : if True then no attached certificate in mail
236 x509_crypt_certfiles: the certificates file to encrypt the messages
237 with can be a file name or a list of
238 file names (PEM format)
239
240 Examples:
241
242 #Create Mail object with authentication data for remote server:
243 mail = Mail('example.com:25', 'me@example.com', 'me:password')
244 """
245
246 settings = self.settings = Settings()
247 settings.server = server
248 settings.sender = sender
249 settings.login = login
250 settings.tls = tls
251 settings.hostname = None
252 settings.ssl = False
253 settings.cipher_type = None
254 settings.gpg_home = None
255 settings.sign = True
256 settings.sign_passphrase = None
257 settings.encrypt = True
258 settings.x509_sign_keyfile = None
259 settings.x509_sign_certfile = None
260 settings.x509_nocerts = False
261 settings.x509_crypt_certfiles = None
262 settings.debug = False
263 settings.lock_keys = True
264 self.result = {}
265 self.error = None
266
267 - def send(
268 self,
269 to,
270 subject = '[no subject]',
271 message = '[no message]',
272 attachments=None,
273 cc=None,
274 bcc=None,
275 reply_to=None,
276 sender=None,
277 encoding='utf-8',
278 raw=False,
279 headers={}
280 ):
281 """
282 Sends an email using data specified in constructor
283
284 Arguments:
285
286 to: list or tuple of receiver addresses; will also accept single
287 object
288 subject: subject of the email
289 message: email body text; depends on type of passed object:
290 if 2-list or 2-tuple is passed: first element will be
291 source of plain text while second of html text;
292 otherwise: object will be the only source of plain text
293 and html source will be set to None;
294 If text or html source is:
295 None: content part will be ignored,
296 string: content part will be set to it,
297 file-like object: content part will be fetched from
298 it using it's read() method
299 attachments: list or tuple of Mail.Attachment objects; will also
300 accept single object
301 cc: list or tuple of carbon copy receiver addresses; will also
302 accept single object
303 bcc: list or tuple of blind carbon copy receiver addresses; will
304 also accept single object
305 reply_to: address to which reply should be composed
306 encoding: encoding of all strings passed to this method (including
307 message bodies)
308 headers: dictionary of headers to refine the headers just before
309 sending mail, e.g. {'Return-Path' : 'bounces@example.org'}
310
311 Examples:
312
313 #Send plain text message to single address:
314 mail.send('you@example.com',
315 'Message subject',
316 'Plain text body of the message')
317
318 #Send html message to single address:
319 mail.send('you@example.com',
320 'Message subject',
321 '<html>Plain text body of the message</html>')
322
323 #Send text and html message to three addresses (two in cc):
324 mail.send('you@example.com',
325 'Message subject',
326 ('Plain text body', '<html>html body</html>'),
327 cc=['other1@example.com', 'other2@example.com'])
328
329 #Send html only message with image attachment available from
330 the message by 'photo' content id:
331 mail.send('you@example.com',
332 'Message subject',
333 (None, '<html><img src="cid:photo" /></html>'),
334 Mail.Attachment('/path/to/photo.jpg'
335 content_id='photo'))
336
337 #Send email with two attachments and no body text
338 mail.send('you@example.com,
339 'Message subject',
340 None,
341 [Mail.Attachment('/path/to/fist.file'),
342 Mail.Attachment('/path/to/second.file')])
343
344 Returns True on success, False on failure.
345
346 Before return, method updates two object's fields:
347 self.result: return value of smtplib.SMTP.sendmail() or GAE's
348 mail.send_mail() method
349 self.error: Exception message or None if above was successful
350 """
351
352
353 Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
354
355 def encode_header(key):
356 if [c for c in key if 32 > ord(c) or ord(c) > 127]:
357 return Header.Header(key.encode('utf-8'), 'utf-8')
358 else:
359 return key
360
361
362 def encoded_or_raw(text):
363 if raw:
364 text = encode_header(text)
365 return text
366
367 sender = sender or self.settings.sender
368
369 if not isinstance(self.settings.server, str):
370 raise Exception('Server address not specified')
371 if not isinstance(sender, str):
372 raise Exception('Sender address not specified')
373
374 if not raw and attachments:
375
376 payload_in = MIMEMultipart.MIMEMultipart('mixed')
377 elif raw:
378
379 if not isinstance(message, basestring):
380 message = message.read()
381 if isinstance(message, unicode):
382 text = message.encode('utf-8')
383 elif not encoding == 'utf-8':
384 text = message.decode(encoding).encode('utf-8')
385 else:
386 text = message
387
388
389
390 payload_in = MIMEText.MIMEText(text)
391 if to:
392 if not isinstance(to, (list, tuple)):
393 to = [to]
394 else:
395 raise Exception('Target receiver address not specified')
396 if cc:
397 if not isinstance(cc, (list, tuple)):
398 cc = [cc]
399 if bcc:
400 if not isinstance(bcc, (list, tuple)):
401 bcc = [bcc]
402 if message is None:
403 text = html = None
404 elif isinstance(message, (list, tuple)):
405 text, html = message
406 elif message.strip().startswith('<html') and \
407 message.strip().endswith('</html>'):
408 text = self.settings.server == 'gae' and message or None
409 html = message
410 else:
411 text = message
412 html = None
413
414 if (not text is None or not html is None) and (not raw):
415
416 if not text is None:
417 if not isinstance(text, basestring):
418 text = text.read()
419 if isinstance(text, unicode):
420 text = text.encode('utf-8')
421 elif not encoding == 'utf-8':
422 text = text.decode(encoding).encode('utf-8')
423 if not html is None:
424 if not isinstance(html, basestring):
425 html = html.read()
426 if isinstance(html, unicode):
427 html = html.encode('utf-8')
428 elif not encoding == 'utf-8':
429 html = html.decode(encoding).encode('utf-8')
430
431
432 if text and html:
433
434 attachment = MIMEMultipart.MIMEMultipart('alternative')
435 attachment.attach(MIMEText.MIMEText(text, _charset='utf-8'))
436 attachment.attach(
437 MIMEText.MIMEText(html, 'html', _charset='utf-8'))
438 elif text:
439 attachment = MIMEText.MIMEText(text, _charset='utf-8')
440 elif html:
441 attachment = \
442 MIMEText.MIMEText(html, 'html', _charset='utf-8')
443
444 if attachments:
445
446
447 payload_in.attach(attachment)
448 else:
449
450 payload_in = attachment
451
452 if (attachments is None) or raw:
453 pass
454 elif isinstance(attachments, (list, tuple)):
455 for attachment in attachments:
456 payload_in.attach(attachment)
457 else:
458 payload_in.attach(attachments)
459
460
461
462
463 cipher_type = self.settings.cipher_type
464 sign = self.settings.sign
465 sign_passphrase = self.settings.sign_passphrase
466 encrypt = self.settings.encrypt
467
468
469
470 if cipher_type == 'gpg':
471 if self.settings.gpg_home:
472
473 import os
474 os.environ['GNUPGHOME'] = self.settings.gpg_home
475 if not sign and not encrypt:
476 self.error = "No sign and no encrypt is set but cipher type to gpg"
477 return False
478
479
480 from pyme import core, errors
481 from pyme.constants.sig import mode
482
483
484
485 if sign:
486 import string
487 core.check_version(None)
488 pin = string.replace(payload_in.as_string(), '\n', '\r\n')
489 plain = core.Data(pin)
490 sig = core.Data()
491 c = core.Context()
492 c.set_armor(1)
493 c.signers_clear()
494
495 for sigkey in c.op_keylist_all(sender, 1):
496 if sigkey.can_sign:
497 c.signers_add(sigkey)
498 if not c.signers_enum(0):
499 self.error = 'No key for signing [%s]' % sender
500 return False
501 c.set_passphrase_cb(lambda x, y, z: sign_passphrase)
502 try:
503
504 c.op_sign(plain, sig, mode.DETACH)
505 sig.seek(0, 0)
506
507 payload = MIMEMultipart.MIMEMultipart('signed',
508 boundary=None,
509 _subparts=None,
510 **dict(
511 micalg="pgp-sha1",
512 protocol="application/pgp-signature"))
513
514 payload.attach(payload_in)
515
516 p = MIMEBase.MIMEBase("application", 'pgp-signature')
517 p.set_payload(sig.read())
518 payload.attach(p)
519
520 payload_in = payload
521 except errors.GPGMEError, ex:
522 self.error = "GPG error: %s" % ex.getstring()
523 return False
524
525
526
527 if encrypt:
528 core.check_version(None)
529 plain = core.Data(payload_in.as_string())
530 cipher = core.Data()
531 c = core.Context()
532 c.set_armor(1)
533
534 recipients = []
535 rec = to[:]
536 if cc:
537 rec.extend(cc)
538 if bcc:
539 rec.extend(bcc)
540 for addr in rec:
541 c.op_keylist_start(addr, 0)
542 r = c.op_keylist_next()
543 if r is None:
544 self.error = 'No key for [%s]' % addr
545 return False
546 recipients.append(r)
547 try:
548
549 c.op_encrypt(recipients, 1, plain, cipher)
550 cipher.seek(0, 0)
551
552 payload = MIMEMultipart.MIMEMultipart('encrypted',
553 boundary=None,
554 _subparts=None,
555 **dict(protocol="application/pgp-encrypted"))
556 p = MIMEBase.MIMEBase("application", 'pgp-encrypted')
557 p.set_payload("Version: 1\r\n")
558 payload.attach(p)
559 p = MIMEBase.MIMEBase("application", 'octet-stream')
560 p.set_payload(cipher.read())
561 payload.attach(p)
562 except errors.GPGMEError, ex:
563 self.error = "GPG error: %s" % ex.getstring()
564 return False
565
566
567
568 elif cipher_type == 'x509':
569 if not sign and not encrypt:
570 self.error = "No sign and no encrypt is set but cipher type to x509"
571 return False
572 x509_sign_keyfile = self.settings.x509_sign_keyfile
573 if self.settings.x509_sign_certfile:
574 x509_sign_certfile = self.settings.x509_sign_certfile
575 else:
576
577
578 x509_sign_certfile = self.settings.x509_sign_keyfile
579
580 x509_crypt_certfiles = self.settings.x509_crypt_certfiles
581 x509_nocerts = self.settings.x509_nocerts
582
583
584 try:
585 from M2Crypto import BIO, SMIME, X509
586 except Exception, e:
587 self.error = "Can't load M2Crypto module"
588 return False
589 msg_bio = BIO.MemoryBuffer(payload_in.as_string())
590 s = SMIME.SMIME()
591
592
593 if sign:
594
595 try:
596 s.load_key(x509_sign_keyfile, x509_sign_certfile,
597 callback=lambda x: sign_passphrase)
598 except Exception, e:
599 self.error = "Something went wrong on certificate / private key loading: <%s>" % str(e)
600 return False
601 try:
602 if x509_nocerts:
603 flags = SMIME.PKCS7_NOCERTS
604 else:
605 flags = 0
606 if not encrypt:
607 flags += SMIME.PKCS7_DETACHED
608 p7 = s.sign(msg_bio, flags=flags)
609 msg_bio = BIO.MemoryBuffer(payload_in.as_string(
610 ))
611 except Exception, e:
612 self.error = "Something went wrong on signing: <%s> %s" % (
613 str(e), str(flags))
614 return False
615
616
617 if encrypt:
618 try:
619 sk = X509.X509_Stack()
620 if not isinstance(x509_crypt_certfiles, (list, tuple)):
621 x509_crypt_certfiles = [x509_crypt_certfiles]
622
623
624 for x in x509_crypt_certfiles:
625 sk.push(X509.load_cert(x))
626 s.set_x509_stack(sk)
627
628 s.set_cipher(SMIME.Cipher('des_ede3_cbc'))
629 tmp_bio = BIO.MemoryBuffer()
630 if sign:
631 s.write(tmp_bio, p7)
632 else:
633 tmp_bio.write(payload_in.as_string())
634 p7 = s.encrypt(tmp_bio)
635 except Exception, e:
636 self.error = "Something went wrong on encrypting: <%s>" % str(e)
637 return False
638
639
640 out = BIO.MemoryBuffer()
641 if encrypt:
642 s.write(out, p7)
643 else:
644 if sign:
645 s.write(out, p7, msg_bio, SMIME.PKCS7_DETACHED)
646 else:
647 out.write('\r\n')
648 out.write(payload_in.as_string())
649 out.close()
650 st = str(out.read())
651 payload = message_from_string(st)
652 else:
653
654 payload = payload_in
655
656 payload['From'] = encoded_or_raw(sender.decode(encoding))
657 origTo = to[:]
658 if to:
659 payload['To'] = encoded_or_raw(', '.join(to).decode(encoding))
660 if reply_to:
661 payload['Reply-To'] = encoded_or_raw(reply_to.decode(encoding))
662 if cc:
663 payload['Cc'] = encoded_or_raw(', '.join(cc).decode(encoding))
664 to.extend(cc)
665 if bcc:
666 to.extend(bcc)
667 payload['Subject'] = encoded_or_raw(subject.decode(encoding))
668 payload['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000",
669 time.gmtime())
670 for k, v in headers.iteritems():
671 payload[k] = encoded_or_raw(v.decode(encoding))
672 result = {}
673 try:
674 if self.settings.server == 'logging':
675 logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' %
676 ('-' * 40, sender,
677 ', '.join(to), subject,
678 text or html, '-' * 40))
679 elif self.settings.server == 'gae':
680 xcc = dict()
681 if cc:
682 xcc['cc'] = cc
683 if bcc:
684 xcc['bcc'] = bcc
685 if reply_to:
686 xcc['reply_to'] = reply_to
687 from google.appengine.api import mail
688 attachments = attachments and [(a.my_filename, a.my_payload) for a in attachments if not raw]
689 if attachments:
690 result = mail.send_mail(
691 sender=sender, to=origTo,
692 subject=subject, body=text, html=html,
693 attachments=attachments, **xcc)
694 elif html and (not raw):
695 result = mail.send_mail(
696 sender=sender, to=origTo,
697 subject=subject, body=text, html=html, **xcc)
698 else:
699 result = mail.send_mail(
700 sender=sender, to=origTo,
701 subject=subject, body=text, **xcc)
702 else:
703 smtp_args = self.settings.server.split(':')
704 if self.settings.ssl:
705 server = smtplib.SMTP_SSL(*smtp_args)
706 else:
707 server = smtplib.SMTP(*smtp_args)
708 if self.settings.tls and not self.settings.ssl:
709 server.ehlo(self.settings.hostname)
710 server.starttls()
711 server.ehlo(self.settings.hostname)
712 if self.settings.login:
713 server.login(*self.settings.login.split(':', 1))
714 result = server.sendmail(
715 sender, to, payload.as_string())
716 server.quit()
717 except Exception, e:
718 logger.warn('Mail.send failure:%s' % e)
719 self.result = result
720 self.error = e
721 return False
722 self.result = result
723 self.error = None
724 return True
725
728
729 """
730 Usage:
731
732 form = FORM(Recaptcha(public_key='...',private_key='...'))
733
734 or
735
736 form = SQLFORM(...)
737 form.append(Recaptcha(public_key='...',private_key='...'))
738 """
739
740 API_SSL_SERVER = 'https://www.google.com/recaptcha/api'
741 API_SERVER = 'http://www.google.com/recaptcha/api'
742 VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify'
743
744 - def __init__(
745 self,
746 request=None,
747 public_key='',
748 private_key='',
749 use_ssl=False,
750 error=None,
751 error_message='invalid',
752 label='Verify:',
753 options=''
754 ):
755 self.request_vars = request and request.vars or current.request.vars
756 self.remote_addr = request.env.remote_addr
757 self.public_key = public_key
758 self.private_key = private_key
759 self.use_ssl = use_ssl
760 self.error = error
761 self.errors = Storage()
762 self.error_message = error_message
763 self.components = []
764 self.attributes = {}
765 self.label = label
766 self.options = options
767 self.comment = ''
768
770
771
772
773 recaptcha_challenge_field = \
774 self.request_vars.recaptcha_challenge_field
775 recaptcha_response_field = \
776 self.request_vars.recaptcha_response_field
777 private_key = self.private_key
778 remoteip = self.remote_addr
779 if not (recaptcha_response_field and recaptcha_challenge_field
780 and len(recaptcha_response_field)
781 and len(recaptcha_challenge_field)):
782 self.errors['captcha'] = self.error_message
783 return False
784 params = urllib.urlencode({
785 'privatekey': private_key,
786 'remoteip': remoteip,
787 'challenge': recaptcha_challenge_field,
788 'response': recaptcha_response_field,
789 })
790 request = urllib2.Request(
791 url=self.VERIFY_SERVER,
792 data=params,
793 headers={'Content-type': 'application/x-www-form-urlencoded',
794 'User-agent': 'reCAPTCHA Python'})
795 httpresp = urllib2.urlopen(request)
796 return_values = httpresp.read().splitlines()
797 httpresp.close()
798 return_code = return_values[0]
799 if return_code == 'true':
800 del self.request_vars.recaptcha_challenge_field
801 del self.request_vars.recaptcha_response_field
802 self.request_vars.captcha = ''
803 return True
804 else:
805
806
807 self.error = return_values[1]
808 self.errors['captcha'] = self.error_message
809 return False
810
812 public_key = self.public_key
813 use_ssl = self.use_ssl
814 error_param = ''
815 if self.error:
816 error_param = '&error=%s' % self.error
817 if use_ssl:
818 server = self.API_SSL_SERVER
819 else:
820 server = self.API_SERVER
821 captcha = DIV(
822 SCRIPT("var RecaptchaOptions = {%s};" % self.options),
823 SCRIPT(_type="text/javascript",
824 _src="%s/challenge?k=%s%s" % (server, public_key, error_param)),
825 TAG.noscript(
826 IFRAME(
827 _src="%s/noscript?k=%s%s" % (
828 server, public_key, error_param),
829 _height="300", _width="500", _frameborder="0"), BR(),
830 INPUT(
831 _type='hidden', _name='recaptcha_response_field',
832 _value='manual_challenge')), _id='recaptcha')
833 if not self.errors.captcha:
834 return XML(captcha).xml()
835 else:
836 captcha.append(DIV(self.errors['captcha'], _class='error'))
837 return XML(captcha).xml()
838
839
840 -def addrow(form, a, b, c, style, _id, position=-1):
841 if style == "divs":
842 form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'),
843 DIV(b, _class='w2p_fw'),
844 DIV(c, _class='w2p_fc'),
845 _id=_id))
846 elif style == "table2cols":
847 form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
848 TD(c, _class='w2p_fc')))
849 form[0].insert(position + 1, TR(TD(b, _class='w2p_fw'),
850 _colspan=2, _id=_id))
851 elif style == "ul":
852 form[0].insert(position, LI(DIV(LABEL(a), _class='w2p_fl'),
853 DIV(b, _class='w2p_fw'),
854 DIV(c, _class='w2p_fc'),
855 _id=_id))
856 elif style == "bootstrap":
857 form[0].insert(position, DIV(LABEL(a, _class='control-label'),
858 DIV(b, SPAN(c, _class='inline-help'),
859 _class='controls'),
860 _class='control-group', _id=_id))
861 else:
862 form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
863 TD(b, _class='w2p_fw'),
864 TD(c, _class='w2p_fc'), _id=_id))
865
866
867 -class Auth(object):
868
869 default_settings = dict(
870 hideerror=False,
871 password_min_length=4,
872 cas_maps=None,
873 reset_password_requires_verification=False,
874 registration_requires_verification=False,
875 registration_requires_approval=False,
876 login_after_registration=False,
877 login_after_password_change=True,
878 alternate_requires_registration=False,
879 create_user_groups="user_%(id)s",
880 everybody_group_id=None,
881 manager_group_role=None,
882 login_captcha=None,
883 register_captcha=None,
884 retrieve_username_captcha=None,
885 retrieve_password_captcha=None,
886 captcha=None,
887 expiration=3600,
888 long_expiration=3600 * 30 * 24,
889 remember_me_form=True,
890 allow_basic_login=False,
891 allow_basic_login_only=False,
892 on_failed_authentication=lambda x: redirect(x),
893 formstyle="table3cols",
894 label_separator=": ",
895 allow_delete_accounts=False,
896 password_field='password',
897 table_user_name='auth_user',
898 table_group_name='auth_group',
899 table_membership_name='auth_membership',
900 table_permission_name='auth_permission',
901 table_event_name='auth_event',
902 table_cas_name='auth_cas',
903 table_user=None,
904 table_group=None,
905 table_membership=None,
906 table_permission=None,
907 table_event=None,
908 table_cas=None,
909 showid=False,
910 use_username=False,
911 login_email_validate=True,
912 login_userfield=None,
913 logout_onlogout=None,
914 register_fields=None,
915 register_verify_password=True,
916 profile_fields=None,
917 email_case_sensitive=True,
918 username_case_sensitive=True,
919 update_fields = ['email'],
920 ondelete="CASCADE",
921 client_side = True,
922 wiki = Settings(),
923 )
924
925 default_messages = dict(
926 login_button='Login',
927 register_button='Register',
928 password_reset_button='Request reset password',
929 password_change_button='Change password',
930 profile_save_button='Apply changes',
931 submit_button='Submit',
932 verify_password='Verify Password',
933 delete_label='Check to delete',
934 function_disabled='Function disabled',
935 access_denied='Insufficient privileges',
936 registration_verifying='Registration needs verification',
937 registration_pending='Registration is pending approval',
938 login_disabled='Login disabled by administrator',
939 logged_in='Logged in',
940 email_sent='Email sent',
941 unable_to_send_email='Unable to send email',
942 email_verified='Email verified',
943 logged_out='Logged out',
944 registration_successful='Registration successful',
945 invalid_email='Invalid email',
946 unable_send_email='Unable to send email',
947 invalid_login='Invalid login',
948 invalid_user='Invalid user',
949 invalid_password='Invalid password',
950 is_empty="Cannot be empty",
951 mismatched_password="Password fields don't match",
952 verify_email='Click on the link %(link)s to verify your email',
953 verify_email_subject='Email verification',
954 username_sent='Your username was emailed to you',
955 new_password_sent='A new password was emailed to you',
956 password_changed='Password changed',
957 retrieve_username='Your username is: %(username)s',
958 retrieve_username_subject='Username retrieve',
959 retrieve_password='Your password is: %(password)s',
960 retrieve_password_subject='Password retrieve',
961 reset_password=
962 'Click on the link %(link)s to reset your password',
963 reset_password_subject='Password reset',
964 invalid_reset_password='Invalid reset password',
965 profile_updated='Profile updated',
966 new_password='New password',
967 old_password='Old password',
968 group_description='Group uniquely assigned to user %(id)s',
969 register_log='User %(id)s Registered',
970 login_log='User %(id)s Logged-in',
971 login_failed_log=None,
972 logout_log='User %(id)s Logged-out',
973 profile_log='User %(id)s Profile updated',
974 verify_email_log='User %(id)s Verification email sent',
975 retrieve_username_log='User %(id)s Username retrieved',
976 retrieve_password_log='User %(id)s Password retrieved',
977 reset_password_log='User %(id)s Password reset',
978 change_password_log='User %(id)s Password changed',
979 add_group_log='Group %(group_id)s created',
980 del_group_log='Group %(group_id)s deleted',
981 add_membership_log=None,
982 del_membership_log=None,
983 has_membership_log=None,
984 add_permission_log=None,
985 del_permission_log=None,
986 has_permission_log=None,
987 impersonate_log='User %(id)s is impersonating %(other_id)s',
988 label_first_name='First name',
989 label_last_name='Last name',
990 label_username='Username',
991 label_email='E-mail',
992 label_password='Password',
993 label_registration_key='Registration key',
994 label_reset_password_key='Reset Password key',
995 label_registration_id='Registration identifier',
996 label_role='Role',
997 label_description='Description',
998 label_user_id='User ID',
999 label_group_id='Group ID',
1000 label_name='Name',
1001 label_table_name='Object or table name',
1002 label_record_id='Record ID',
1003 label_time_stamp='Timestamp',
1004 label_client_ip='Client IP',
1005 label_origin='Origin',
1006 label_remember_me="Remember me (for 30 days)",
1007 verify_password_comment='please input your password again',
1008 )
1009
1010 """
1011 Class for authentication, authorization, role based access control.
1012
1013 Includes:
1014
1015 - registration and profile
1016 - login and logout
1017 - username and password retrieval
1018 - event logging
1019 - role creation and assignment
1020 - user defined group/role based permission
1021
1022 Authentication Example:
1023
1024 from contrib.utils import *
1025 mail=Mail()
1026 mail.settings.server='smtp.gmail.com:587'
1027 mail.settings.sender='you@somewhere.com'
1028 mail.settings.login='username:password'
1029 auth=Auth(db)
1030 auth.settings.mailer=mail
1031 # auth.settings....=...
1032 auth.define_tables()
1033 def authentication():
1034 return dict(form=auth())
1035
1036 exposes:
1037
1038 - http://.../{application}/{controller}/authentication/login
1039 - http://.../{application}/{controller}/authentication/logout
1040 - http://.../{application}/{controller}/authentication/register
1041 - http://.../{application}/{controller}/authentication/verify_email
1042 - http://.../{application}/{controller}/authentication/retrieve_username
1043 - http://.../{application}/{controller}/authentication/retrieve_password
1044 - http://.../{application}/{controller}/authentication/reset_password
1045 - http://.../{application}/{controller}/authentication/profile
1046 - http://.../{application}/{controller}/authentication/change_password
1047
1048 On registration a group with role=new_user.id is created
1049 and user is given membership of this group.
1050
1051 You can create a group with:
1052
1053 group_id=auth.add_group('Manager', 'can access the manage action')
1054 auth.add_permission(group_id, 'access to manage')
1055
1056 Here \"access to manage\" is just a user defined string.
1057 You can give access to a user:
1058
1059 auth.add_membership(group_id, user_id)
1060
1061 If user id is omitted, the logged in user is assumed
1062
1063 Then you can decorate any action:
1064
1065 @auth.requires_permission('access to manage')
1066 def manage():
1067 return dict()
1068
1069 You can restrict a permission to a specific table:
1070
1071 auth.add_permission(group_id, 'edit', db.sometable)
1072 @auth.requires_permission('edit', db.sometable)
1073
1074 Or to a specific record:
1075
1076 auth.add_permission(group_id, 'edit', db.sometable, 45)
1077 @auth.requires_permission('edit', db.sometable, 45)
1078
1079 If authorization is not granted calls:
1080
1081 auth.settings.on_failed_authorization
1082
1083 Other options:
1084
1085 auth.settings.mailer=None
1086 auth.settings.expiration=3600 # seconds
1087
1088 ...
1089
1090 ### these are messages that can be customized
1091 ...
1092 """
1093
1094 @staticmethod
1105
1106 - def url(self, f=None, args=None, vars=None, scheme=False):
1107 if args is None:
1108 args = []
1109 if vars is None:
1110 vars = {}
1111 return URL(c=self.settings.controller,
1112 f=f, args=args, vars=vars, scheme=scheme)
1113
1116
1117 - def __init__(self, environment=None, db=None, mailer=True,
1118 hmac_key=None, controller='default', function='user',
1119 cas_provider=None, signature=True, secure=False):
1120 """
1121 auth=Auth(db)
1122
1123 - environment is there for legacy but unused (awful)
1124 - db has to be the database where to create tables for authentication
1125 - mailer=Mail(...) or None (no mailed) or True (make a mailer)
1126 - hmac_key can be a hmac_key or hmac_key=Auth.get_or_create_key()
1127 - controller (where is the user action?)
1128 - cas_provider (delegate authentication to the URL, CAS2)
1129 """
1130
1131 if not db and environment and isinstance(environment, DAL):
1132 db = environment
1133 self.db = db
1134 self.environment = current
1135 request = current.request
1136 session = current.session
1137 auth = session.auth
1138 self.user_groups = auth and auth.user_groups or {}
1139 if secure:
1140 request.requires_https()
1141 if auth and auth.last_visit and auth.last_visit + \
1142 datetime.timedelta(days=0, seconds=auth.expiration) > request.now:
1143 self.user = auth.user
1144
1145 if (request.now - auth.last_visit).seconds > (auth.expiration / 10):
1146 auth.last_visit = request.now
1147 else:
1148 self.user = None
1149 if session.auth:
1150 del session.auth
1151
1152
1153 self.next = current.request.vars._next
1154 if isinstance(self.next, (list, tuple)):
1155 self.next = self.next[0]
1156 url_index = URL(controller, 'index')
1157 url_login = URL(controller, function, args='login')
1158
1159
1160 settings = self.settings = Settings()
1161 settings.update(Auth.default_settings)
1162 settings.update(
1163 cas_domains=[request.env.http_host],
1164 cas_provider=cas_provider,
1165 cas_actions=dict(login='login',
1166 validate='validate',
1167 servicevalidate='serviceValidate',
1168 proxyvalidate='proxyValidate',
1169 logout='logout'),
1170 extra_fields={},
1171 actions_disabled=[],
1172 controller=controller,
1173 function=function,
1174 login_url=url_login,
1175 logged_url=URL(controller, function, args='profile'),
1176 download_url=URL(controller, 'download'),
1177 mailer=(mailer == True) and Mail() or mailer,
1178 on_failed_authorization =
1179 URL(controller, function, args='not_authorized'),
1180 login_next = url_index,
1181 login_onvalidation = [],
1182 login_onaccept = [],
1183 login_onfail = [],
1184 login_methods = [self],
1185 login_form = self,
1186 logout_next = url_index,
1187 logout_onlogout = None,
1188 register_next = url_index,
1189 register_onvalidation = [],
1190 register_onaccept = [],
1191 verify_email_next = url_login,
1192 verify_email_onaccept = [],
1193 profile_next = url_index,
1194 profile_onvalidation = [],
1195 profile_onaccept = [],
1196 retrieve_username_next = url_index,
1197 retrieve_password_next = url_index,
1198 request_reset_password_next = url_login,
1199 reset_password_next = url_index,
1200 change_password_next = url_index,
1201 change_password_onvalidation = [],
1202 change_password_onaccept = [],
1203 retrieve_password_onvalidation = [],
1204 reset_password_onvalidation = [],
1205 reset_password_onaccept = [],
1206 hmac_key = hmac_key,
1207 )
1208 settings.lock_keys = True
1209
1210
1211 messages = self.messages = Messages(current.T)
1212 messages.update(Auth.default_messages)
1213 messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'),
1214 'Please ',
1215 A('login',
1216 _href=self.settings.login_url +
1217 ('?_next=' + urllib.quote(current.request.env.http_web2py_component_location))
1218 if current.request.env.http_web2py_component_location else ''),
1219 ' to view this content.',
1220 _class='not-authorized alert alert-block'))
1221 messages.lock_keys = True
1222
1223
1224 response = current.response
1225 if auth and auth.remember:
1226
1227 response.cookies[response.session_id_name]["expires"] = \
1228 auth.expiration
1229 if signature:
1230 self.define_signature()
1231 else:
1232 self.signature = None
1233
1235 "accessor for auth.user_id"
1236 return self.user and self.user.id or None
1237
1238 user_id = property(_get_user_id, doc="user.id or None")
1239
1241 return self.db[self.settings.table_user_name]
1242
1244 return self.db[self.settings.table_group_name]
1245
1247 return self.db[self.settings.table_membership_name]
1248
1250 return self.db[self.settings.table_permission_name]
1251
1253 return self.db[self.settings.table_event_name]
1254
1257
1258 - def _HTTP(self, *a, **b):
1259 """
1260 only used in lambda: self._HTTP(404)
1261 """
1262
1263 raise HTTP(*a, **b)
1264
1266 """
1267 usage:
1268
1269 def authentication(): return dict(form=auth())
1270 """
1271
1272 request = current.request
1273 args = request.args
1274 if not args:
1275 redirect(self.url(args='login', vars=request.vars))
1276 elif args[0] in self.settings.actions_disabled:
1277 raise HTTP(404)
1278 if args[0] in ('login', 'logout', 'register', 'verify_email',
1279 'retrieve_username', 'retrieve_password',
1280 'reset_password', 'request_reset_password',
1281 'change_password', 'profile', 'groups',
1282 'impersonate', 'not_authorized'):
1283 if len(request.args) >= 2 and args[0] == 'impersonate':
1284 return getattr(self, args[0])(request.args[1])
1285 else:
1286 return getattr(self, args[0])()
1287 elif args[0] == 'cas' and not self.settings.cas_provider:
1288 if args(1) == self.settings.cas_actions['login']:
1289 return self.cas_login(version=2)
1290 elif args(1) == self.settings.cas_actions['validate']:
1291 return self.cas_validate(version=1)
1292 elif args(1) == self.settings.cas_actions['servicevalidate']:
1293 return self.cas_validate(version=2, proxy=False)
1294 elif args(1) == self.settings.cas_actions['proxyvalidate']:
1295 return self.cas_validate(version=2, proxy=True)
1296 elif args(1) == self.settings.cas_actions['logout']:
1297 return self.logout(next=request.vars.service or DEFAULT)
1298 else:
1299 raise HTTP(404)
1300
1301 - def navbar(self, prefix='Welcome', action=None,
1302 separators=(' [ ', ' | ', ' ] '), user_identifier=DEFAULT,
1303 referrer_actions=DEFAULT, mode='default'):
1304 def Anr(*a,**b):
1305 b['_rel']='nofollow'
1306 return A(*a,**b)
1307 referrer_actions = [] if not referrer_actions else referrer_actions
1308 request = current.request
1309 asdropdown = (mode == 'dropdown')
1310 T = current.T
1311 if isinstance(prefix, str):
1312 prefix = T(prefix)
1313 if prefix:
1314 prefix = prefix.strip() + ' '
1315 if not action:
1316 action = self.url(self.settings.function)
1317 s1, s2, s3 = separators
1318 if URL() == action:
1319 next = ''
1320 else:
1321 next = '?_next=' + urllib.quote(URL(args=request.args,
1322 vars=request.get_vars))
1323 href = lambda function: '%s/%s%s' % (action, function,
1324 next if referrer_actions is DEFAULT or function in referrer_actions else '')
1325
1326 if self.user_id:
1327 if user_identifier is DEFAULT:
1328 user_identifier = '%(first_name)s'
1329 if callable(user_identifier):
1330 user_identifier = user_identifier(self.user)
1331 elif ((isinstance(user_identifier, str) or
1332 type(user_identifier).__name__ == 'lazyT') and
1333 re.search(r'%\(.+\)s', user_identifier)):
1334 user_identifier = user_identifier % self.user
1335 if not user_identifier:
1336 user_identifier = ''
1337 logout = Anr(T('Logout'), _href='%s/logout?_next=%s' %
1338 (action, urllib.quote(self.settings.logout_next)))
1339 profile = Anr(T('Profile'), _href=href('profile'))
1340 password = Anr(T('Password'), _href=href('change_password'))
1341 bar = SPAN(
1342 prefix, user_identifier, s1, logout, s3, _class='auth_navbar')
1343
1344 if asdropdown:
1345 logout = LI(Anr(I(_class='icon-off'), ' ' + T('Logout'), _href='%s/logout?_next=%s' %
1346 (action, urllib.quote(self.settings.logout_next))))
1347 profile = LI(Anr(I(_class='icon-user'), ' ' +
1348 T('Profile'), _href=href('profile')))
1349 password = LI(Anr(I(_class='icon-lock'), ' ' +
1350 T('Password'), _href=href('change_password')))
1351 bar = UL(logout, _class='dropdown-menu')
1352
1353
1354 if not 'profile' in self.settings.actions_disabled:
1355 if not asdropdown:
1356 bar.insert(-1, s2)
1357 bar.insert(-1, profile)
1358 if not 'change_password' in self.settings.actions_disabled:
1359 if not asdropdown:
1360 bar.insert(-1, s2)
1361 bar.insert(-1, password)
1362 else:
1363 login = Anr(T('Login'), _href=href('login'))
1364 register = Anr(T('Register'), _href=href('register'))
1365 retrieve_username = Anr(
1366 T('Forgot username?'), _href=href('retrieve_username'))
1367 lost_password = Anr(
1368 T('Lost password?'), _href=href('request_reset_password'))
1369 bar = SPAN(s1, login, s3, _class='auth_navbar')
1370
1371 if asdropdown:
1372 login = LI(Anr(I(_class='icon-off'), ' ' + T('Login'), _href=href('login')))
1373 register = LI(Anr(I(_class='icon-user'),
1374 ' ' + T('Register'), _href=href('register')))
1375 retrieve_username = LI(Anr(I(_class='icon-edit'), ' ' + T(
1376 'Forgot username?'), _href=href('retrieve_username')))
1377 lost_password = LI(Anr(I(_class='icon-lock'), ' ' + T(
1378 'Lost password?'), _href=href('request_reset_password')))
1379 bar = UL(login, _class='dropdown-menu')
1380
1381
1382 if not 'register' in self.settings.actions_disabled:
1383 if not asdropdown:
1384 bar.insert(-1, s2)
1385 bar.insert(-1, register)
1386 if self.settings.use_username and not 'retrieve_username' \
1387 in self.settings.actions_disabled:
1388 if not asdropdown:
1389 bar.insert(-1, s2)
1390 bar.insert(-1, retrieve_username)
1391 if not 'request_reset_password' \
1392 in self.settings.actions_disabled:
1393 if not asdropdown:
1394 bar.insert(-1, s2)
1395 bar.insert(-1, lost_password)
1396
1397 if asdropdown:
1398 bar.insert(-1, LI('', _class='divider'))
1399 if self.user_id:
1400 bar = LI(Anr(prefix, user_identifier, _href='#'),
1401 bar, _class='dropdown')
1402 else:
1403 bar = LI(Anr(T('Login'), _href='#'),
1404 bar, _class='dropdown')
1405 return bar
1406
1408
1409 if type(migrate).__name__ == 'str':
1410 return (migrate + tablename + '.table')
1411 elif migrate == False:
1412 return False
1413 else:
1414 return True
1415
1416 - def enable_record_versioning(self,
1417 tables,
1418 archive_db=None,
1419 archive_names='%(tablename)s_archive',
1420 current_record='current_record'):
1421 """
1422 to enable full record versioning (including auth tables):
1423
1424 auth = Auth(db)
1425 auth.define_tables(signature=True)
1426 # define our own tables
1427 db.define_table('mything',Field('name'),auth.signature)
1428 auth.enable_record_versioning(tables=db)
1429
1430 tables can be the db (all table) or a list of tables.
1431 only tables with modified_by and modified_on fiels (as created
1432 by auth.signature) will have versioning. Old record versions will be
1433 in table 'mything_archive' automatically defined.
1434
1435 when you enable enable_record_versioning, records are never
1436 deleted but marked with is_active=False.
1437
1438 enable_record_versioning enables a common_filter for
1439 every table that filters out records with is_active = False
1440
1441 Important: If you use auth.enable_record_versioning,
1442 do not use auth.archive or you will end up with duplicates.
1443 auth.archive does explicitly what enable_record_versioning
1444 does automatically.
1445
1446 """
1447 tables = [table for table in tables]
1448 for table in tables:
1449 if 'modified_on' in table.fields() and not current_record in table.fields():
1450 table._enable_record_versioning(
1451 archive_db=archive_db,
1452 archive_name=archive_names,
1453 current_record=current_record)
1454
1464
1465 def represent(id, record=None, s=settings):
1466 try:
1467 user = s.table_user(id)
1468 return '%s %s' % (user.get("first_name", user.get("email")),
1469 user.get("last_name", ''))
1470 except:
1471 return id
1472 ondelete = self.settings.ondelete
1473 self.signature = db.Table(
1474 self.db, 'auth_signature',
1475 Field('is_active', 'boolean',
1476 default=True,
1477 readable=False, writable=False,
1478 label=T('Is Active')),
1479 Field('created_on', 'datetime',
1480 default=request.now,
1481 writable=False, readable=False,
1482 label=T('Created On')),
1483 Field('created_by',
1484 reference_user,
1485 default=lazy_user, represent=represent,
1486 writable=False, readable=False,
1487 label=T('Created By'), ondelete=ondelete),
1488 Field('modified_on', 'datetime',
1489 update=request.now, default=request.now,
1490 writable=False, readable=False,
1491 label=T('Modified On')),
1492 Field('modified_by',
1493 reference_user, represent=represent,
1494 default=lazy_user, update=lazy_user,
1495 writable=False, readable=False,
1496 label=T('Modified By'), ondelete=ondelete))
1497
1498 - def define_tables(self, username=None, signature=None,
1499 migrate=True, fake_migrate=False):
1500 """
1501 to be called unless tables are defined manually
1502
1503 usages:
1504
1505 # defines all needed tables and table files
1506 # 'myprefix_auth_user.table', ...
1507 auth.define_tables(migrate='myprefix_')
1508
1509 # defines all needed tables without migration/table files
1510 auth.define_tables(migrate=False)
1511
1512 """
1513
1514 db = self.db
1515 settings = self.settings
1516 if username is None:
1517 username = settings.use_username
1518 else:
1519 settings.use_username = username
1520 if not self.signature:
1521 self.define_signature()
1522 if signature == True:
1523 signature_list = [self.signature]
1524 elif not signature:
1525 signature_list = []
1526 elif isinstance(signature, self.db.Table):
1527 signature_list = [signature]
1528 else:
1529 signature_list = signature
1530 is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty)
1531 is_crypted = CRYPT(key=settings.hmac_key,
1532 min_length=settings.password_min_length)
1533 is_unique_email = [
1534 IS_EMAIL(error_message=self.messages.invalid_email),
1535 IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name)]
1536 if not settings.email_case_sensitive:
1537 is_unique_email.insert(1, IS_LOWER())
1538 if not settings.table_user_name in db.tables:
1539 passfield = settings.password_field
1540 extra_fields = settings.extra_fields.get(
1541 settings.table_user_name, []) + signature_list
1542 if username or settings.cas_provider:
1543 is_unique_username = \
1544 [IS_MATCH('[\w\.\-]+'),
1545 IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name)]
1546 if not settings.username_case_sensitive:
1547 is_unique_username.insert(1, IS_LOWER())
1548 db.define_table(
1549 settings.table_user_name,
1550 Field('first_name', length=128, default='',
1551 label=self.messages.label_first_name,
1552 requires=is_not_empty),
1553 Field('last_name', length=128, default='',
1554 label=self.messages.label_last_name,
1555 requires=is_not_empty),
1556 Field('email', length=512, default='',
1557 label=self.messages.label_email,
1558 requires=is_unique_email),
1559 Field('username', length=128, default='',
1560 label=self.messages.label_username,
1561 requires=is_unique_username),
1562 Field(passfield, 'password', length=512,
1563 readable=False, label=self.messages.label_password,
1564 requires=[is_crypted]),
1565 Field('registration_key', length=512,
1566 writable=False, readable=False, default='',
1567 label=self.messages.label_registration_key),
1568 Field('reset_password_key', length=512,
1569 writable=False, readable=False, default='',
1570 label=self.messages.label_reset_password_key),
1571 Field('registration_id', length=512,
1572 writable=False, readable=False, default='',
1573 label=self.messages.label_registration_id),
1574 *extra_fields,
1575 **dict(
1576 migrate=self.__get_migrate(settings.table_user_name,
1577 migrate),
1578 fake_migrate=fake_migrate,
1579 format='%(username)s'))
1580 else:
1581 db.define_table(
1582 settings.table_user_name,
1583 Field('first_name', length=128, default='',
1584 label=self.messages.label_first_name,
1585 requires=is_not_empty),
1586 Field('last_name', length=128, default='',
1587 label=self.messages.label_last_name,
1588 requires=is_not_empty),
1589 Field('email', length=512, default='',
1590 label=self.messages.label_email,
1591 requires=is_unique_email),
1592 Field(passfield, 'password', length=512,
1593 readable=False, label=self.messages.label_password,
1594 requires=[is_crypted]),
1595 Field('registration_key', length=512,
1596 writable=False, readable=False, default='',
1597 label=self.messages.label_registration_key),
1598 Field('reset_password_key', length=512,
1599 writable=False, readable=False, default='',
1600 label=self.messages.label_reset_password_key),
1601 Field('registration_id', length=512,
1602 writable=False, readable=False, default='',
1603 label=self.messages.label_registration_id),
1604 *extra_fields,
1605 **dict(
1606 migrate=self.__get_migrate(settings.table_user_name,
1607 migrate),
1608 fake_migrate=fake_migrate,
1609 format='%(first_name)s %(last_name)s (%(id)s)'))
1610 reference_table_user = 'reference %s' % settings.table_user_name
1611 if not settings.table_group_name in db.tables:
1612 extra_fields = settings.extra_fields.get(
1613 settings.table_group_name, []) + signature_list
1614 db.define_table(
1615 settings.table_group_name,
1616 Field('role', length=512, default='',
1617 label=self.messages.label_role,
1618 requires=IS_NOT_IN_DB(
1619 db, '%s.role' % settings.table_group_name)),
1620 Field('description', 'text',
1621 label=self.messages.label_description),
1622 *extra_fields,
1623 **dict(
1624 migrate=self.__get_migrate(
1625 settings.table_group_name, migrate),
1626 fake_migrate=fake_migrate,
1627 format='%(role)s (%(id)s)'))
1628 reference_table_group = 'reference %s' % settings.table_group_name
1629 if not settings.table_membership_name in db.tables:
1630 extra_fields = settings.extra_fields.get(
1631 settings.table_membership_name, []) + signature_list
1632 db.define_table(
1633 settings.table_membership_name,
1634 Field('user_id', reference_table_user,
1635 label=self.messages.label_user_id),
1636 Field('group_id', reference_table_group,
1637 label=self.messages.label_group_id),
1638 *extra_fields,
1639 **dict(
1640 migrate=self.__get_migrate(
1641 settings.table_membership_name, migrate),
1642 fake_migrate=fake_migrate))
1643 if not settings.table_permission_name in db.tables:
1644 extra_fields = settings.extra_fields.get(
1645 settings.table_permission_name, []) + signature_list
1646 db.define_table(
1647 settings.table_permission_name,
1648 Field('group_id', reference_table_group,
1649 label=self.messages.label_group_id),
1650 Field('name', default='default', length=512,
1651 label=self.messages.label_name,
1652 requires=is_not_empty),
1653 Field('table_name', length=512,
1654 label=self.messages.label_table_name),
1655 Field('record_id', 'integer', default=0,
1656 label=self.messages.label_record_id,
1657 requires=IS_INT_IN_RANGE(0, 10 ** 9)),
1658 *extra_fields,
1659 **dict(
1660 migrate=self.__get_migrate(
1661 settings.table_permission_name, migrate),
1662 fake_migrate=fake_migrate))
1663 if not settings.table_event_name in db.tables:
1664 db.define_table(
1665 settings.table_event_name,
1666 Field('time_stamp', 'datetime',
1667 default=current.request.now,
1668 label=self.messages.label_time_stamp),
1669 Field('client_ip',
1670 default=current.request.client,
1671 label=self.messages.label_client_ip),
1672 Field('user_id', reference_table_user, default=None,
1673 label=self.messages.label_user_id),
1674 Field('origin', default='auth', length=512,
1675 label=self.messages.label_origin,
1676 requires=is_not_empty),
1677 Field('description', 'text', default='',
1678 label=self.messages.label_description,
1679 requires=is_not_empty),
1680 *settings.extra_fields.get(settings.table_event_name, []),
1681 **dict(
1682 migrate=self.__get_migrate(
1683 settings.table_event_name, migrate),
1684 fake_migrate=fake_migrate))
1685 now = current.request.now
1686 if settings.cas_domains:
1687 if not settings.table_cas_name in db.tables:
1688 db.define_table(
1689 settings.table_cas_name,
1690 Field('user_id', reference_table_user, default=None,
1691 label=self.messages.label_user_id),
1692 Field('created_on', 'datetime', default=now),
1693 Field('service', requires=IS_URL()),
1694 Field('ticket'),
1695 Field('renew', 'boolean', default=False),
1696 *settings.extra_fields.get(settings.table_cas_name, []),
1697 **dict(
1698 migrate=self.__get_migrate(
1699 settings.table_cas_name, migrate),
1700 fake_migrate=fake_migrate))
1701 if not db._lazy_tables:
1702 settings.table_user = db[settings.table_user_name]
1703 settings.table_group = db[settings.table_group_name]
1704 settings.table_membership = db[settings.table_membership_name]
1705 settings.table_permission = db[settings.table_permission_name]
1706 settings.table_event = db[settings.table_event_name]
1707 if settings.cas_domains:
1708 settings.table_cas = db[settings.table_cas_name]
1709
1710 if settings.cas_provider:
1711 settings.actions_disabled = \
1712 ['profile', 'register', 'change_password',
1713 'request_reset_password', 'retrieve_username']
1714 from gluon.contrib.login_methods.cas_auth import CasAuth
1715 maps = settings.cas_maps
1716 if not maps:
1717 table_user = self.table_user()
1718 maps = dict((name, lambda v, n=name: v.get(n, None)) for name in
1719 table_user.fields if name != 'id'
1720 and table_user[name].readable)
1721 maps['registration_id'] = \
1722 lambda v, p=settings.cas_provider: '%s/%s' % (p, v['user'])
1723 actions = [settings.cas_actions['login'],
1724 settings.cas_actions['servicevalidate'],
1725 settings.cas_actions['logout']]
1726 settings.login_form = CasAuth(
1727 casversion=2,
1728 urlbase=settings.cas_provider,
1729 actions=actions,
1730 maps=maps)
1731 return self
1732
1733 - def log_event(self, description, vars=None, origin='auth'):
1734 """
1735 usage:
1736
1737 auth.log_event(description='this happened', origin='auth')
1738 """
1739 if not description:
1740 return
1741 elif self.is_logged_in():
1742 user_id = self.user.id
1743 else:
1744 user_id = None
1745 vars = vars or {}
1746 self.table_event().insert(
1747 description=str(description % vars),
1748 origin=origin, user_id=user_id)
1749
1751 """
1752 Used for alternate login methods:
1753 If the user exists already then password is updated.
1754 If the user doesn't yet exist, then they are created.
1755 """
1756 table_user = self.table_user()
1757 user = None
1758 checks = []
1759
1760 for fieldname in ['registration_id', 'username', 'email']:
1761 if fieldname in table_user.fields() and \
1762 keys.get(fieldname, None):
1763 checks.append(fieldname)
1764 value = keys[fieldname]
1765 user = table_user(**{fieldname: value})
1766 if user:
1767 break
1768 if not checks:
1769 return None
1770 if not 'registration_id' in keys:
1771 keys['registration_id'] = keys[checks[0]]
1772
1773
1774 if 'registration_id' in checks \
1775 and user \
1776 and user.registration_id \
1777 and ('registration_id' not in keys or user.registration_id != str(keys['registration_id'])):
1778 user = None
1779 if user:
1780 update_keys = dict(registration_id=keys['registration_id'])
1781 for key in update_fields:
1782 if key in keys:
1783 update_keys[key] = keys[key]
1784 user.update_record(**update_keys)
1785 elif checks:
1786 if not 'first_name' in keys and 'first_name' in table_user.fields:
1787 guess = keys.get('email', 'anonymous').split('@')[0]
1788 keys['first_name'] = keys.get('username', guess)
1789 user_id = table_user.insert(**table_user._filter_fields(keys))
1790 user = self.user = table_user[user_id]
1791 if self.settings.create_user_groups:
1792 group_id = self.add_group(
1793 self.settings.create_user_groups % user)
1794 self.add_membership(group_id, user_id)
1795 if self.settings.everybody_group_id:
1796 self.add_membership(self.settings.everybody_group_id, user_id)
1797 return user
1798
1799 - def basic(self, basic_auth_realm=False):
1800 """
1801 perform basic login.
1802
1803 :param basic_auth_realm: optional basic http authentication realm.
1804 :type basic_auth_realm: str or unicode or function or callable or boolean.
1805
1806 reads current.request.env.http_authorization
1807 and returns basic_allowed,basic_accepted,user.
1808
1809 if basic_auth_realm is defined is a callable it's return value
1810 is used to set the basic authentication realm, if it's a string
1811 its content is used instead. Otherwise basic authentication realm
1812 is set to the application name.
1813 If basic_auth_realm is None or False (the default) the behavior
1814 is to skip sending any challenge.
1815
1816 """
1817 if not self.settings.allow_basic_login:
1818 return (False, False, False)
1819 basic = current.request.env.http_authorization
1820 if basic_auth_realm:
1821 if callable(basic_auth_realm):
1822 basic_auth_realm = basic_auth_auth()
1823 elif isinstance(basic_auth_realm, (unicode, str)):
1824 basic_realm = unicode(basic_auth_realm)
1825 elif basic_auth_realm is True:
1826 basic_realm = u'' + current.request.application
1827 http_401 = HTTP(401, u'Not Authorized',
1828 **{'WWW-Authenticate': u'Basic realm="' + basic_realm + '"'})
1829 if not basic or not basic[:6].lower() == 'basic ':
1830 if basic_auth_realm:
1831 raise http_401
1832 return (True, False, False)
1833 (username, sep, password) = base64.b64decode(basic[6:]).partition(':')
1834 is_valid_user = sep and self.login_bare(username, password)
1835 if not is_valid_user and basic_auth_realm:
1836 raise http_401
1837 return (True, True, is_valid_user)
1838
1859
1861 """
1862 logins user as specified by usernname (or email) and password
1863 """
1864 table_user = self.table_user()
1865 if self.settings.login_userfield:
1866 userfield = self.settings.login_userfield
1867 elif 'username' in table_user.fields:
1868 userfield = 'username'
1869 else:
1870 userfield = 'email'
1871 passfield = self.settings.password_field
1872 user = self.db(table_user[userfield] == username).select().first()
1873 if user and user.get(passfield, False):
1874 password = table_user[passfield].validate(password)[0]
1875 if not user.registration_key and password == user[passfield]:
1876 self.login_user(user)
1877 return user
1878 else:
1879
1880 for login_method in self.settings.login_methods:
1881 if login_method != self and login_method(username, password):
1882 self.user = username
1883 return username
1884 return False
1885
1924 if self.is_logged_in() and not 'renew' in request.vars:
1925 return allow_access()
1926 elif not self.is_logged_in() and 'gateway' in request.vars:
1927 redirect(service)
1928
1929 def cas_onaccept(form, onaccept=onaccept):
1930 if not onaccept is DEFAULT:
1931 onaccept(form)
1932 return allow_access(interactivelogin=True)
1933 return self.login(next, onvalidation, cas_onaccept, log)
1934
1936 request = current.request
1937 db, table = self.db, self.table_cas()
1938 current.response.headers['Content-Type'] = 'text'
1939 ticket = request.vars.ticket
1940 renew = 'renew' in request.vars
1941 row = table(ticket=ticket)
1942 success = False
1943 if row:
1944 if self.settings.login_userfield:
1945 userfield = self.settings.login_userfield
1946 elif 'username' in table.fields:
1947 userfield = 'username'
1948 else:
1949 userfield = 'email'
1950
1951 if ticket[0:3] == 'ST-' and \
1952 not ((row.renew and renew) ^ renew):
1953 user = self.table_user()(row.user_id)
1954 row.delete_record()
1955 success = True
1956
1957 def build_response(body):
1958 return '<?xml version="1.0" encoding="UTF-8"?>\n' +\
1959 TAG['cas:serviceResponse'](
1960 body, **{'_xmlns:cas': 'http://www.yale.edu/tp/cas'}).xml()
1961 if success:
1962 if version == 1:
1963 message = 'yes\n%s' % user[userfield]
1964 else:
1965 username = user.get('username', user[userfield])
1966 message = build_response(
1967 TAG['cas:authenticationSuccess'](
1968 TAG['cas:user'](username),
1969 *[TAG['cas:' + field.name](user[field.name])
1970 for field in self.table_user()
1971 if field.readable]))
1972 else:
1973 if version == 1:
1974 message = 'no\n'
1975 elif row:
1976 message = build_response(TAG['cas:authenticationFailure']())
1977 else:
1978 message = build_response(
1979 TAG['cas:authenticationFailure'](
1980 'Ticket %s not recognized' % ticket,
1981 _code='INVALID TICKET'))
1982 raise HTTP(200, message)
1983
1991 """
1992 returns a login form
1993
1994 method: Auth.login([next=DEFAULT [, onvalidation=DEFAULT
1995 [, onaccept=DEFAULT [, log=DEFAULT]]]])
1996
1997 """
1998
1999 table_user = self.table_user()
2000 if self.settings.login_userfield:
2001 username = self.settings.login_userfield
2002 elif 'username' in table_user.fields:
2003 username = 'username'
2004 else:
2005 username = 'email'
2006 if 'username' in table_user.fields or \
2007 not self.settings.login_email_validate:
2008 tmpvalidator = IS_NOT_EMPTY(error_message=self.messages.is_empty)
2009 else:
2010 tmpvalidator = IS_EMAIL(error_message=self.messages.invalid_email)
2011 old_requires = table_user[username].requires
2012 table_user[username].requires = tmpvalidator
2013
2014 request = current.request
2015 response = current.response
2016 session = current.session
2017
2018 passfield = self.settings.password_field
2019 try:
2020 table_user[passfield].requires[-1].min_length = 0
2021 except:
2022 pass
2023
2024
2025 if self.next:
2026 session._auth_next = self.next
2027 elif session._auth_next:
2028 self.next = session._auth_next
2029
2030
2031 if next is DEFAULT:
2032 next = self.next or self.settings.login_next
2033 if onvalidation is DEFAULT:
2034 onvalidation = self.settings.login_onvalidation
2035 if onaccept is DEFAULT:
2036 onaccept = self.settings.login_onaccept
2037 if log is DEFAULT:
2038 log = self.messages.login_log
2039
2040 onfail = self.settings.login_onfail
2041
2042 user = None
2043
2044
2045 if self.settings.login_form == self:
2046 form = SQLFORM(
2047 table_user,
2048 fields=[username, passfield],
2049 hidden=dict(_next=next),
2050 showid=self.settings.showid,
2051 submit_button=self.messages.login_button,
2052 delete_label=self.messages.delete_label,
2053 formstyle=self.settings.formstyle,
2054 separator=self.settings.label_separator
2055 )
2056
2057 if self.settings.remember_me_form:
2058
2059 if self.settings.formstyle != 'bootstrap':
2060 addrow(form, XML(" "),
2061 DIV(XML(" "),
2062 INPUT(_type='checkbox',
2063 _class='checkbox',
2064 _id="auth_user_remember",
2065 _name="remember",
2066 ),
2067 XML(" "),
2068 LABEL(
2069 self.messages.label_remember_me,
2070 _for="auth_user_remember",
2071 )), "",
2072 self.settings.formstyle,
2073 'auth_user_remember__row')
2074 elif self.settings.formstyle == 'bootstrap':
2075 addrow(form,
2076 "",
2077 LABEL(
2078 INPUT(_type='checkbox',
2079 _id="auth_user_remember",
2080 _name="remember"),
2081 self.messages.label_remember_me,
2082 _class="checkbox"),
2083 "",
2084 self.settings.formstyle,
2085 'auth_user_remember__row')
2086
2087 captcha = self.settings.login_captcha or \
2088 (self.settings.login_captcha != False and self.settings.captcha)
2089 if captcha:
2090 addrow(form, captcha.label, captcha, captcha.comment,
2091 self.settings.formstyle, 'captcha__row')
2092 accepted_form = False
2093
2094 if form.accepts(request, session,
2095 formname='login', dbio=False,
2096 onvalidation=onvalidation,
2097 hideerror=self.settings.hideerror):
2098
2099 accepted_form = True
2100
2101 user = self.db(table_user[username]
2102 == form.vars[username]).select().first()
2103 if user:
2104
2105 temp_user = user
2106 if temp_user.registration_key == 'pending':
2107 response.flash = self.messages.registration_pending
2108 return form
2109 elif temp_user.registration_key in ('disabled', 'blocked'):
2110 response.flash = self.messages.login_disabled
2111 return form
2112 elif not temp_user.registration_key is None and \
2113 temp_user.registration_key.strip():
2114 response.flash = \
2115 self.messages.registration_verifying
2116 return form
2117
2118
2119 user = None
2120 for login_method in self.settings.login_methods:
2121 if login_method != self and \
2122 login_method(request.vars[username],
2123 request.vars[passfield]):
2124 if not self in self.settings.login_methods:
2125
2126 form.vars[passfield] = None
2127 user = self.get_or_create_user(
2128 form.vars, self.settings.update_fields)
2129 break
2130 if not user:
2131
2132 if self.settings.login_methods[0] == self:
2133
2134 if form.vars.get(passfield, '') == temp_user[passfield]:
2135
2136 user = temp_user
2137 else:
2138
2139 if not self.settings.alternate_requires_registration:
2140
2141 for login_method in self.settings.login_methods:
2142 if login_method != self and \
2143 login_method(request.vars[username],
2144 request.vars[passfield]):
2145 if not self in self.settings.login_methods:
2146
2147 form.vars[passfield] = None
2148 user = self.get_or_create_user(
2149 form.vars, self.settings.update_fields)
2150 break
2151 if not user:
2152 self.log_event(self.messages.login_failed_log,
2153 request.post_vars)
2154
2155 session.flash = self.messages.invalid_login
2156 callback(onfail, None)
2157 redirect(
2158 self.url(args=request.args, vars=request.get_vars),
2159 client_side=self.settings.client_side)
2160
2161 else:
2162
2163 cas = self.settings.login_form
2164 cas_user = cas.get_user()
2165
2166 if cas_user:
2167 cas_user[passfield] = None
2168 user = self.get_or_create_user(
2169 table_user._filter_fields(cas_user),
2170 self.settings.update_fields)
2171 elif hasattr(cas, 'login_form'):
2172 return cas.login_form()
2173 else:
2174
2175 next = self.url(self.settings.function, args='login')
2176 redirect(cas.login_url(next),
2177 client_side=self.settings.client_side)
2178
2179
2180 if user:
2181 user = Row(table_user._filter_fields(user, id=True))
2182
2183
2184 self.login_user(user)
2185 session.auth.expiration = \
2186 request.vars.get('remember', False) and \
2187 self.settings.long_expiration or \
2188 self.settings.expiration
2189 session.auth.remember = 'remember' in request.vars
2190 self.log_event(log, user)
2191 session.flash = self.messages.logged_in
2192
2193
2194 if self.settings.login_form == self:
2195 if accepted_form:
2196 callback(onaccept, form)
2197 if next == session._auth_next:
2198 session._auth_next = None
2199 next = replace_id(next, form)
2200 redirect(next, client_side=self.settings.client_side)
2201
2202 table_user[username].requires = old_requires
2203 return form
2204 elif user:
2205 callback(onaccept, None)
2206
2207 if next == session._auth_next:
2208 del session._auth_next
2209 redirect(next, client_side=self.settings.client_side)
2210
2212 """
2213 logout and redirects to login
2214
2215 method: Auth.logout ([next=DEFAULT[, onlogout=DEFAULT[,
2216 log=DEFAULT]]])
2217
2218 """
2219
2220 if next is DEFAULT:
2221 next = self.settings.logout_next
2222 if onlogout is DEFAULT:
2223 onlogout = self.settings.logout_onlogout
2224 if onlogout:
2225 onlogout(self.user)
2226 if log is DEFAULT:
2227 log = self.messages.logout_log
2228 if self.user:
2229 self.log_event(log, self.user)
2230 if self.settings.login_form != self:
2231 cas = self.settings.login_form
2232 cas_user = cas.get_user()
2233 if cas_user:
2234 next = cas.logout_url(next)
2235
2236 current.session.auth = None
2237 current.session.flash = self.messages.logged_out
2238 if not next is None:
2239 redirect(next)
2240
2248 """
2249 returns a registration form
2250
2251 method: Auth.register([next=DEFAULT [, onvalidation=DEFAULT
2252 [, onaccept=DEFAULT [, log=DEFAULT]]]])
2253
2254 """
2255
2256 table_user = self.table_user()
2257 request = current.request
2258 response = current.response
2259 session = current.session
2260 if self.is_logged_in():
2261 redirect(self.settings.logged_url,
2262 client_side=self.settings.client_side)
2263 if next is DEFAULT:
2264 next = self.next or self.settings.register_next
2265 if onvalidation is DEFAULT:
2266 onvalidation = self.settings.register_onvalidation
2267 if onaccept is DEFAULT:
2268 onaccept = self.settings.register_onaccept
2269 if log is DEFAULT:
2270 log = self.messages.register_log
2271
2272 table_user = self.table_user()
2273 if self.settings.login_userfield:
2274 username = self.settings.login_userfield
2275 elif 'username' in table_user.fields:
2276 username = 'username'
2277 else:
2278 username = 'email'
2279
2280
2281 unique_validator = IS_NOT_IN_DB(self.db, table_user[username])
2282 if not table_user[username].requires:
2283 table_user[username].requires = unique_validator
2284 elif isinstance(table_user[username].requires, (list, tuple)):
2285 if not any([isinstance(validator, IS_NOT_IN_DB) for validator in
2286 table_user[username].requires]):
2287 if isinstance(table_user[username].requires, list):
2288 table_user[username].requires.append(unique_validator)
2289 else:
2290 table_user[username].requires += (unique_validator, )
2291 elif not isinstance(table_user[username].requires, IS_NOT_IN_DB):
2292 table_user[username].requires = [table_user[username].requires,
2293 unique_validator]
2294
2295 passfield = self.settings.password_field
2296 formstyle = self.settings.formstyle
2297 form = SQLFORM(table_user,
2298 fields=self.settings.register_fields,
2299 hidden=dict(_next=next),
2300 showid=self.settings.showid,
2301 submit_button=self.messages.register_button,
2302 delete_label=self.messages.delete_label,
2303 formstyle=formstyle,
2304 separator=self.settings.label_separator
2305 )
2306 if self.settings.register_verify_password:
2307 for i, row in enumerate(form[0].components):
2308 item = row.element('input', _name=passfield)
2309 if item:
2310 form.custom.widget.password_two = \
2311 INPUT(_name="password_two", _type="password",
2312 requires=IS_EXPR(
2313 'value==%s' %
2314 repr(request.vars.get(passfield, None)),
2315 error_message=self.messages.mismatched_password))
2316
2317 if formstyle == 'bootstrap':
2318 form.custom.widget.password_two[
2319 '_class'] = 'input-xlarge'
2320
2321 addrow(
2322 form, self.messages.verify_password +
2323 self.settings.label_separator,
2324 form.custom.widget.password_two,
2325 self.messages.verify_password_comment,
2326 formstyle,
2327 '%s_%s__row' % (table_user, 'password_two'),
2328 position=i + 1)
2329 break
2330 captcha = self.settings.register_captcha or self.settings.captcha
2331 if captcha:
2332 addrow(form, captcha.label, captcha,
2333 captcha.comment, self.settings.formstyle, 'captcha__row')
2334
2335 table_user.registration_key.default = key = web2py_uuid()
2336 if form.accepts(request, session, formname='register',
2337 onvalidation=onvalidation, hideerror=self.settings.hideerror):
2338 description = self.messages.group_description % form.vars
2339 if self.settings.create_user_groups:
2340 group_id = self.add_group(
2341 self.settings.create_user_groups % form.vars, description)
2342 self.add_membership(group_id, form.vars.id)
2343 if self.settings.everybody_group_id:
2344 self.add_membership(
2345 self.settings.everybody_group_id, form.vars.id)
2346 if self.settings.registration_requires_verification:
2347 link = self.url(
2348 self.settings.function, args=('verify_email', key), scheme=True)
2349
2350 if not self.settings.mailer or \
2351 not self.settings.mailer.send(
2352 to=form.vars.email,
2353 subject=self.messages.verify_email_subject,
2354 message=self.messages.verify_email
2355 % dict(key=key, link=link)):
2356 self.db.rollback()
2357 response.flash = self.messages.unable_send_email
2358 return form
2359 session.flash = self.messages.email_sent
2360 if self.settings.registration_requires_approval and \
2361 not self.settings.registration_requires_verification:
2362 table_user[form.vars.id] = dict(registration_key='pending')
2363 session.flash = self.messages.registration_pending
2364 elif (not self.settings.registration_requires_verification or
2365 self.settings.login_after_registration):
2366 if not self.settings.registration_requires_verification:
2367 table_user[form.vars.id] = dict(registration_key='')
2368 session.flash = self.messages.registration_successful
2369 user = self.db(
2370 table_user[username] == form.vars[username]
2371 ).select().first()
2372 self.login_user(user)
2373 session.flash = self.messages.logged_in
2374 self.log_event(log, form.vars)
2375 callback(onaccept, form)
2376 if not next:
2377 next = self.url(args=request.args)
2378 else:
2379 next = replace_id(next, form)
2380 redirect(next, client_side=self.settings.client_side)
2381 return form
2382
2384 """
2385 checks if the user is logged in and returns True/False.
2386 if so user is in auth.user as well as in session.auth.user
2387 """
2388
2389 if self.user:
2390 return True
2391 return False
2392
2430
2438 """
2439 returns a form to retrieve the user username
2440 (only if there is a username field)
2441
2442 method: Auth.retrieve_username([next=DEFAULT
2443 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2444
2445 """
2446
2447 table_user = self.table_user()
2448 if not 'username' in table_user.fields:
2449 raise HTTP(404)
2450 request = current.request
2451 response = current.response
2452 session = current.session
2453 captcha = self.settings.retrieve_username_captcha or \
2454 (self.settings.retrieve_username_captcha != False and self.settings.captcha)
2455 if not self.settings.mailer:
2456 response.flash = self.messages.function_disabled
2457 return ''
2458 if next is DEFAULT:
2459 next = self.next or self.settings.retrieve_username_next
2460 if onvalidation is DEFAULT:
2461 onvalidation = self.settings.retrieve_username_onvalidation
2462 if onaccept is DEFAULT:
2463 onaccept = self.settings.retrieve_username_onaccept
2464 if log is DEFAULT:
2465 log = self.messages.retrieve_username_log
2466 old_requires = table_user.email.requires
2467 table_user.email.requires = [IS_IN_DB(self.db, table_user.email,
2468 error_message=self.messages.invalid_email)]
2469 form = SQLFORM(table_user,
2470 fields=['email'],
2471 hidden=dict(_next=next),
2472 showid=self.settings.showid,
2473 submit_button=self.messages.submit_button,
2474 delete_label=self.messages.delete_label,
2475 formstyle=self.settings.formstyle,
2476 separator=self.settings.label_separator
2477 )
2478 if captcha:
2479 addrow(form, captcha.label, captcha,
2480 captcha.comment, self.settings.formstyle, 'captcha__row')
2481
2482 if form.accepts(request, session,
2483 formname='retrieve_username', dbio=False,
2484 onvalidation=onvalidation, hideerror=self.settings.hideerror):
2485 user = table_user(email=form.vars.email)
2486 if not user:
2487 current.session.flash = \
2488 self.messages.invalid_email
2489 redirect(self.url(args=request.args))
2490 username = user.username
2491 self.settings.mailer.send(to=form.vars.email,
2492 subject=self.messages.retrieve_username_subject,
2493 message=self.messages.retrieve_username
2494 % dict(username=username))
2495 session.flash = self.messages.email_sent
2496 self.log_event(log, user)
2497 callback(onaccept, form)
2498 if not next:
2499 next = self.url(args=request.args)
2500 else:
2501 next = replace_id(next, form)
2502 redirect(next)
2503 table_user.email.requires = old_requires
2504 return form
2505
2507 import string
2508 import random
2509 password = ''
2510 specials = r'!#$*'
2511 for i in range(0, 3):
2512 password += random.choice(string.lowercase)
2513 password += random.choice(string.uppercase)
2514 password += random.choice(string.digits)
2515 password += random.choice(specials)
2516 return ''.join(random.sample(password, len(password)))
2517
2525 """
2526 returns a form to reset the user password (deprecated)
2527
2528 method: Auth.reset_password_deprecated([next=DEFAULT
2529 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2530
2531 """
2532
2533 table_user = self.table_user()
2534 request = current.request
2535 response = current.response
2536 session = current.session
2537 if not self.settings.mailer:
2538 response.flash = self.messages.function_disabled
2539 return ''
2540 if next is DEFAULT:
2541 next = self.next or self.settings.retrieve_password_next
2542 if onvalidation is DEFAULT:
2543 onvalidation = self.settings.retrieve_password_onvalidation
2544 if onaccept is DEFAULT:
2545 onaccept = self.settings.retrieve_password_onaccept
2546 if log is DEFAULT:
2547 log = self.messages.retrieve_password_log
2548 old_requires = table_user.email.requires
2549 table_user.email.requires = [IS_IN_DB(self.db, table_user.email,
2550 error_message=self.messages.invalid_email)]
2551 form = SQLFORM(table_user,
2552 fields=['email'],
2553 hidden=dict(_next=next),
2554 showid=self.settings.showid,
2555 submit_button=self.messages.submit_button,
2556 delete_label=self.messages.delete_label,
2557 formstyle=self.settings.formstyle,
2558 separator=self.settings.label_separator
2559 )
2560 if form.accepts(request, session,
2561 formname='retrieve_password', dbio=False,
2562 onvalidation=onvalidation, hideerror=self.settings.hideerror):
2563 user = table_user(email=form.vars.email)
2564 if not user:
2565 current.session.flash = \
2566 self.messages.invalid_email
2567 redirect(self.url(args=request.args))
2568 elif user.registration_key in ('pending', 'disabled', 'blocked'):
2569 current.session.flash = \
2570 self.messages.registration_pending
2571 redirect(self.url(args=request.args))
2572 password = self.random_password()
2573 passfield = self.settings.password_field
2574 d = dict(
2575 passfield=str(table_user[passfield].validate(password)[0]),
2576 registration_key='')
2577 user.update_record(**d)
2578 if self.settings.mailer and \
2579 self.settings.mailer.send(to=form.vars.email,
2580 subject=self.messages.retrieve_password_subject,
2581 message=self.messages.retrieve_password
2582 % dict(password=password)):
2583 session.flash = self.messages.email_sent
2584 else:
2585 session.flash = self.messages.unable_to_send_email
2586 self.log_event(log, user)
2587 callback(onaccept, form)
2588 if not next:
2589 next = self.url(args=request.args)
2590 else:
2591 next = replace_id(next, form)
2592 redirect(next)
2593 table_user.email.requires = old_requires
2594 return form
2595
2603 """
2604 returns a form to reset the user password
2605
2606 method: Auth.reset_password([next=DEFAULT
2607 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2608
2609 """
2610
2611 table_user = self.table_user()
2612 request = current.request
2613
2614 session = current.session
2615
2616 if next is DEFAULT:
2617 next = self.next or self.settings.reset_password_next
2618 try:
2619 key = request.vars.key or getarg(-1)
2620 t0 = int(key.split('-')[0])
2621 if time.time() - t0 > 60 * 60 * 24:
2622 raise Exception
2623 user = table_user(reset_password_key=key)
2624 if not user:
2625 raise Exception
2626 except Exception:
2627 session.flash = self.messages.invalid_reset_password
2628 redirect(next, client_side=self.settings.client_side)
2629 passfield = self.settings.password_field
2630 form = SQLFORM.factory(
2631 Field('new_password', 'password',
2632 label=self.messages.new_password,
2633 requires=self.table_user()[passfield].requires),
2634 Field('new_password2', 'password',
2635 label=self.messages.verify_password,
2636 requires=[IS_EXPR(
2637 'value==%s' % repr(request.vars.new_password),
2638 self.messages.mismatched_password)]),
2639 submit_button=self.messages.password_reset_button,
2640 hidden=dict(_next=next),
2641 formstyle=self.settings.formstyle,
2642 separator=self.settings.label_separator
2643 )
2644 if form.accepts(request, session,
2645 hideerror=self.settings.hideerror):
2646 user.update_record(
2647 **{passfield: str(form.vars.new_password),
2648 'registration_key': '',
2649 'reset_password_key': ''})
2650 session.flash = self.messages.password_changed
2651 if self.settings.login_after_password_change:
2652 self.login_user(user)
2653 redirect(next, client_side=self.settings.client_side)
2654 return form
2655
2663 """
2664 returns a form to reset the user password
2665
2666 method: Auth.reset_password([next=DEFAULT
2667 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]])
2668
2669 """
2670 table_user = self.table_user()
2671 request = current.request
2672 response = current.response
2673 session = current.session
2674 captcha = self.settings.retrieve_password_captcha or \
2675 (self.settings.retrieve_password_captcha != False and self.settings.captcha)
2676
2677 if next is DEFAULT:
2678 next = self.next or self.settings.request_reset_password_next
2679 if not self.settings.mailer:
2680 response.flash = self.messages.function_disabled
2681 return ''
2682 if onvalidation is DEFAULT:
2683 onvalidation = self.settings.reset_password_onvalidation
2684 if onaccept is DEFAULT:
2685 onaccept = self.settings.reset_password_onaccept
2686 if log is DEFAULT:
2687 log = self.messages.reset_password_log
2688 table_user.email.requires = [
2689 IS_EMAIL(error_message=self.messages.invalid_email),
2690 IS_IN_DB(self.db, table_user.email,
2691 error_message=self.messages.invalid_email)]
2692 form = SQLFORM(table_user,
2693 fields=['email'],
2694 hidden=dict(_next=next),
2695 showid=self.settings.showid,
2696 submit_button=self.messages.password_reset_button,
2697 delete_label=self.messages.delete_label,
2698 formstyle=self.settings.formstyle,
2699 separator=self.settings.label_separator
2700 )
2701 if captcha:
2702 addrow(form, captcha.label, captcha,
2703 captcha.comment, self.settings.formstyle, 'captcha__row')
2704 if form.accepts(request, session,
2705 formname='reset_password', dbio=False,
2706 onvalidation=onvalidation,
2707 hideerror=self.settings.hideerror):
2708 user = table_user(email=form.vars.email)
2709 if not user:
2710 session.flash = self.messages.invalid_email
2711 redirect(self.url(args=request.args),
2712 client_side=self.settings.client_side)
2713 elif user.registration_key in ('pending', 'disabled', 'blocked'):
2714 session.flash = self.messages.registration_pending
2715 redirect(self.url(args=request.args),
2716 client_side=self.settings.client_side)
2717 if self.email_reset_password(user):
2718 session.flash = self.messages.email_sent
2719 else:
2720 session.flash = self.messages.unable_to_send_email
2721 self.log_event(log, user)
2722 callback(onaccept, form)
2723 if not next:
2724 next = self.url(args=request.args)
2725 else:
2726 next = replace_id(next, form)
2727 redirect(next, client_side=self.settings.client_side)
2728
2729 return form
2730
2732 reset_password_key = str(int(time.time())) + '-' + web2py_uuid()
2733 link = self.url(self.settings.function,
2734 args=('reset_password', reset_password_key),
2735 scheme=True)
2736 if self.settings.mailer.send(
2737 to=user.email,
2738 subject=self.messages.reset_password_subject,
2739 message=self.messages.reset_password %
2740 dict(key=reset_password_key, link=link)):
2741 user.update_record(reset_password_key=reset_password_key)
2742 return True
2743 return False
2744
2756
2764 """
2765 returns a form that lets the user change password
2766
2767 method: Auth.change_password([next=DEFAULT[, onvalidation=DEFAULT[,
2768 onaccept=DEFAULT[, log=DEFAULT]]]])
2769 """
2770
2771 if not self.is_logged_in():
2772 redirect(self.settings.login_url,
2773 client_side=self.settings.client_side)
2774 db = self.db
2775 table_user = self.table_user()
2776 s = db(table_user.id == self.user.id)
2777
2778 request = current.request
2779 session = current.session
2780 if next is DEFAULT:
2781 next = self.next or self.settings.change_password_next
2782 if onvalidation is DEFAULT:
2783 onvalidation = self.settings.change_password_onvalidation
2784 if onaccept is DEFAULT:
2785 onaccept = self.settings.change_password_onaccept
2786 if log is DEFAULT:
2787 log = self.messages.change_password_log
2788 passfield = self.settings.password_field
2789 form = SQLFORM.factory(
2790 Field('old_password', 'password',
2791 label=self.messages.old_password,
2792 requires=table_user[passfield].requires),
2793 Field('new_password', 'password',
2794 label=self.messages.new_password,
2795 requires=table_user[passfield].requires),
2796 Field('new_password2', 'password',
2797 label=self.messages.verify_password,
2798 requires=[IS_EXPR(
2799 'value==%s' % repr(request.vars.new_password),
2800 self.messages.mismatched_password)]),
2801 submit_button=self.messages.password_change_button,
2802 hidden=dict(_next=next),
2803 formstyle=self.settings.formstyle,
2804 separator=self.settings.label_separator
2805 )
2806 if form.accepts(request, session,
2807 formname='change_password',
2808 onvalidation=onvalidation,
2809 hideerror=self.settings.hideerror):
2810
2811 if not form.vars['old_password'] == s.select().first()[passfield]:
2812 form.errors['old_password'] = self.messages.invalid_password
2813 else:
2814 d = {passfield: str(form.vars.new_password)}
2815 s.update(**d)
2816 session.flash = self.messages.password_changed
2817 self.log_event(log, self.user)
2818 callback(onaccept, form)
2819 if not next:
2820 next = self.url(args=request.args)
2821 else:
2822 next = replace_id(next, form)
2823 redirect(next, client_side=self.settings.client_side)
2824 return form
2825
2833 """
2834 returns a form that lets the user change his/her profile
2835
2836 method: Auth.profile([next=DEFAULT [, onvalidation=DEFAULT
2837 [, onaccept=DEFAULT [, log=DEFAULT]]]])
2838
2839 """
2840
2841 table_user = self.table_user()
2842 if not self.is_logged_in():
2843 redirect(self.settings.login_url,
2844 client_side=self.settings.client_side)
2845 passfield = self.settings.password_field
2846 table_user[passfield].writable = False
2847 request = current.request
2848 session = current.session
2849 if next is DEFAULT:
2850 next = self.next or self.settings.profile_next
2851 if onvalidation is DEFAULT:
2852 onvalidation = self.settings.profile_onvalidation
2853 if onaccept is DEFAULT:
2854 onaccept = self.settings.profile_onaccept
2855 if log is DEFAULT:
2856 log = self.messages.profile_log
2857 form = SQLFORM(
2858 table_user,
2859 self.user.id,
2860 fields=self.settings.profile_fields,
2861 hidden=dict(_next=next),
2862 showid=self.settings.showid,
2863 submit_button=self.messages.profile_save_button,
2864 delete_label=self.messages.delete_label,
2865 upload=self.settings.download_url,
2866 formstyle=self.settings.formstyle,
2867 separator=self.settings.label_separator,
2868 deletable=self.settings.allow_delete_accounts,
2869 )
2870 if form.accepts(request, session,
2871 formname='profile',
2872 onvalidation=onvalidation,
2873 hideerror=self.settings.hideerror):
2874 self.user.update(table_user._filter_fields(form.vars))
2875 session.flash = self.messages.profile_updated
2876 self.log_event(log, self.user)
2877 callback(onaccept, form)
2878 if form.deleted:
2879 return self.logout()
2880 if not next:
2881 next = self.url(args=request.args)
2882 else:
2883 next = replace_id(next, form)
2884 redirect(next, client_side=self.settings.client_side)
2885 return form
2886
2889
2891 """
2892 usage: POST TO http://..../impersonate request.post_vars.user_id=<id>
2893 set request.post_vars.user_id to 0 to restore original user.
2894
2895 requires impersonator is logged in and
2896 has_permission('impersonate', 'auth_user', user_id)
2897 """
2898 request = current.request
2899 session = current.session
2900 auth = session.auth
2901 table_user = self.table_user()
2902 if not self.is_logged_in():
2903 raise HTTP(401, "Not Authorized")
2904 current_id = auth.user.id
2905 requested_id = user_id
2906 if user_id is DEFAULT:
2907 user_id = current.request.post_vars.user_id
2908 if user_id and user_id != self.user.id and user_id != '0':
2909 if not self.has_permission('impersonate',
2910 self.settings.table_user_name,
2911 user_id):
2912 raise HTTP(403, "Forbidden")
2913 user = table_user(user_id)
2914 if not user:
2915 raise HTTP(401, "Not Authorized")
2916 auth.impersonator = cPickle.dumps(session)
2917 auth.user.update(
2918 table_user._filter_fields(user, True))
2919 self.user = auth.user
2920 onaccept = self.settings.login_onaccept
2921 if onaccept:
2922 form = Storage(dict(vars=self.user))
2923 if not isinstance(onaccept,(list, tuple)):
2924 onaccept = [onaccept]
2925 for callback in onaccept:
2926 callback(form)
2927 log = self.messages.impersonate_log
2928 self.log_event(log, dict(id=current_id, other_id=auth.user.id))
2929 elif user_id in (0, '0'):
2930 if self.is_impersonating():
2931 session.clear()
2932 session.update(cPickle.loads(auth.impersonator))
2933 self.user = session.auth.user
2934 return None
2935 if requested_id is DEFAULT and not request.post_vars:
2936 return SQLFORM.factory(Field('user_id', 'integer'))
2937 return SQLFORM(table_user, user.id, readonly=True)
2938
2953
2975
2977 """
2978 you can change the view for this page to make it look as you like
2979 """
2980 if current.request.ajax:
2981 raise HTTP(403, 'ACCESS DENIED')
2982 return 'ACCESS DENIED'
2983
2984 - def requires(self, condition, requires_login=True, otherwise=None):
2985 """
2986 decorator that prevents access to action if not logged in
2987 """
2988
2989 def decorator(action):
2990
2991 def f(*a, **b):
2992
2993 basic_allowed, basic_accepted, user = self.basic()
2994 user = user or self.user
2995 if requires_login:
2996 if not user:
2997 if current.request.ajax:
2998 raise HTTP(401, self.messages.ajax_failed_authentication)
2999 elif not otherwise is None:
3000 if callable(otherwise):
3001 return otherwise()
3002 redirect(otherwise)
3003 elif self.settings.allow_basic_login_only or \
3004 basic_accepted or current.request.is_restful:
3005 raise HTTP(403, "Not authorized")
3006 else:
3007 next = self.here()
3008 current.session.flash = current.response.flash
3009 return call_or_redirect(
3010 self.settings.on_failed_authentication,
3011 self.settings.login_url +
3012 '?_next=' + urllib.quote(next))
3013
3014 if callable(condition):
3015 flag = condition()
3016 else:
3017 flag = condition
3018 if not flag:
3019 current.session.flash = self.messages.access_denied
3020 return call_or_redirect(
3021 self.settings.on_failed_authorization)
3022 return action(*a, **b)
3023 f.__doc__ = action.__doc__
3024 f.__name__ = action.__name__
3025 f.__dict__.update(action.__dict__)
3026 return f
3027
3028 return decorator
3029
3031 """
3032 decorator that prevents access to action if not logged in
3033 """
3034 return self.requires(True, otherwise=otherwise)
3035
3037 """
3038 decorator that prevents access to action if not logged in or
3039 if user logged in is not a member of group_id.
3040 If role is provided instead of group_id then the
3041 group_id is calculated.
3042 """
3043 def has_membership(self=self, group_id=group_id, role=role):
3044 return self.has_membership(group_id=group_id, role=role)
3045 return self.requires(has_membership, otherwise=otherwise)
3046
3049 """
3050 decorator that prevents access to action if not logged in or
3051 if user logged in is not a member of any group (role) that
3052 has 'name' access to 'table_name', 'record_id'.
3053 """
3054 def has_permission(self=self, name=name, table_name=table_name, record_id=record_id):
3055 return self.has_permission(name, table_name, record_id)
3056 return self.requires(has_permission, otherwise=otherwise)
3057
3059 """
3060 decorator that prevents access to action if not logged in or
3061 if user logged in is not a member of group_id.
3062 If role is provided instead of group_id then the
3063 group_id is calculated.
3064 """
3065 def verify():
3066 return URL.verify(current.request, user_signature=True, hash_vars=hash_vars)
3067 return self.requires(verify, otherwise)
3068
3070 """
3071 creates a group associated to a role
3072 """
3073
3074 group_id = self.table_group().insert(
3075 role=role, description=description)
3076 self.log_event(self.messages.add_group_log,
3077 dict(group_id=group_id, role=role))
3078 return group_id
3079
3089
3091 """
3092 returns the group_id of the group specified by the role
3093 """
3094 rows = self.db(self.table_group().role == role).select()
3095 if not rows:
3096 return None
3097 return rows[0].id
3098
3100 """
3101 returns the group_id of the group uniquely associated to this user
3102 i.e. role=user:[user_id]
3103 """
3104 return self.id_group(self.user_group_role(user_id))
3105
3107 if not self.settings.create_user_groups:
3108 return None
3109 if user_id:
3110 user = self.table_user()[user_id]
3111 else:
3112 user = self.user
3113 return self.settings.create_user_groups % user
3114
3116 """
3117 checks if user is member of group_id or role
3118 """
3119
3120 group_id = group_id or self.id_group(role)
3121 try:
3122 group_id = int(group_id)
3123 except:
3124 group_id = self.id_group(group_id)
3125 if not user_id and self.user:
3126 user_id = self.user.id
3127 membership = self.table_membership()
3128 if group_id and user_id and self.db((membership.user_id == user_id)
3129 & (membership.group_id == group_id)).select():
3130 r = True
3131 else:
3132 r = False
3133 self.log_event(self.messages.has_membership_log,
3134 dict(user_id=user_id, group_id=group_id, check=r))
3135 return r
3136
3138 """
3139 gives user_id membership of group_id or role
3140 if user is None than user_id is that of current logged in user
3141 """
3142
3143 group_id = group_id or self.id_group(role)
3144 try:
3145 group_id = int(group_id)
3146 except:
3147 group_id = self.id_group(group_id)
3148 if not user_id and self.user:
3149 user_id = self.user.id
3150 membership = self.table_membership()
3151 record = membership(user_id=user_id, group_id=group_id)
3152 if record:
3153 return record.id
3154 else:
3155 id = membership.insert(group_id=group_id, user_id=user_id)
3156 self.update_groups()
3157 self.log_event(self.messages.add_membership_log,
3158 dict(user_id=user_id, group_id=group_id))
3159 return id
3160
3162 """
3163 revokes membership from group_id to user_id
3164 if user_id is None than user_id is that of current logged in user
3165 """
3166
3167 group_id = group_id or self.id_group(role)
3168 if not user_id and self.user:
3169 user_id = self.user.id
3170 membership = self.table_membership()
3171 self.log_event(self.messages.del_membership_log,
3172 dict(user_id=user_id, group_id=group_id))
3173 ret = self.db(membership.user_id
3174 == user_id)(membership.group_id
3175 == group_id).delete()
3176 self.update_groups()
3177 return ret
3178
3179 - def has_permission(
3180 self,
3181 name='any',
3182 table_name='',
3183 record_id=0,
3184 user_id=None,
3185 group_id=None,
3186 ):
3187 """
3188 checks if user_id or current logged in user is member of a group
3189 that has 'name' permission on 'table_name' and 'record_id'
3190 if group_id is passed, it checks whether the group has the permission
3191 """
3192
3193 if not group_id and self.settings.everybody_group_id and \
3194 self.has_permission(
3195 name, table_name, record_id, user_id=None,
3196 group_id=self.settings.everybody_group_id):
3197 return True
3198
3199 if not user_id and not group_id and self.user:
3200 user_id = self.user.id
3201 if user_id:
3202 membership = self.table_membership()
3203 rows = self.db(membership.user_id
3204 == user_id).select(membership.group_id)
3205 groups = set([row.group_id for row in rows])
3206 if group_id and not group_id in groups:
3207 return False
3208 else:
3209 groups = set([group_id])
3210 permission = self.table_permission()
3211 rows = self.db(permission.name == name)(permission.table_name
3212 == str(table_name))(permission.record_id
3213 == record_id).select(permission.group_id)
3214 groups_required = set([row.group_id for row in rows])
3215 if record_id:
3216 rows = self.db(permission.name
3217 == name)(permission.table_name
3218 == str(table_name))(permission.record_id
3219 == 0).select(permission.group_id)
3220 groups_required = groups_required.union(set([row.group_id
3221 for row in rows]))
3222 if groups.intersection(groups_required):
3223 r = True
3224 else:
3225 r = False
3226 if user_id:
3227 self.log_event(self.messages.has_permission_log,
3228 dict(user_id=user_id, name=name,
3229 table_name=table_name, record_id=record_id))
3230 return r
3231
3232 - def add_permission(
3233 self,
3234 group_id,
3235 name='any',
3236 table_name='',
3237 record_id=0,
3238 ):
3239 """
3240 gives group_id 'name' access to 'table_name' and 'record_id'
3241 """
3242
3243 permission = self.table_permission()
3244 if group_id == 0:
3245 group_id = self.user_group()
3246 record = self.db(permission.group_id == group_id)(permission.name == name)(permission.table_name == str(table_name))(
3247 permission.record_id == long(record_id)).select().first()
3248 if record:
3249 id = record.id
3250 else:
3251 id = permission.insert(group_id=group_id, name=name,
3252 table_name=str(table_name),
3253 record_id=long(record_id))
3254 self.log_event(self.messages.add_permission_log,
3255 dict(permission_id=id, group_id=group_id,
3256 name=name, table_name=table_name,
3257 record_id=record_id))
3258 return id
3259
3260 - def del_permission(
3261 self,
3262 group_id,
3263 name='any',
3264 table_name='',
3265 record_id=0,
3266 ):
3267 """
3268 revokes group_id 'name' access to 'table_name' and 'record_id'
3269 """
3270
3271 permission = self.table_permission()
3272 self.log_event(self.messages.del_permission_log,
3273 dict(group_id=group_id, name=name,
3274 table_name=table_name, record_id=record_id))
3275 return self.db(permission.group_id == group_id)(permission.name
3276 == name)(permission.table_name
3277 == str(table_name))(permission.record_id
3278 == long(record_id)).delete()
3279
3281 """
3282 returns a query with all accessible records for user_id or
3283 the current logged in user
3284 this method does not work on GAE because uses JOIN and IN
3285
3286 example:
3287
3288 db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL)
3289
3290 """
3291 if not user_id:
3292 user_id = self.user_id
3293 db = self.db
3294 if isinstance(table, str) and table in self.db.tables():
3295 table = self.db[table]
3296 elif isinstance(table, (Set, Query)):
3297
3298 if isinstance(table, Set):
3299 cquery = table.query
3300 else:
3301 cquery = table
3302 tablenames = db._adapter.tables(cquery)
3303 for tablename in tablenames:
3304 cquery &= self.accessible_query(name, tablename,
3305 user_id=user_id)
3306 return cquery
3307 if not isinstance(table, str) and\
3308 self.has_permission(name, table, 0, user_id):
3309 return table.id > 0
3310 membership = self.table_membership()
3311 permission = self.table_permission()
3312 query = table.id.belongs(
3313 db(membership.user_id == user_id)
3314 (membership.group_id == permission.group_id)
3315 (permission.name == name)
3316 (permission.table_name == table)
3317 ._select(permission.record_id))
3318 if self.settings.everybody_group_id:
3319 query |= table.id.belongs(
3320 db(permission.group_id == self.settings.everybody_group_id)
3321 (permission.name == name)
3322 (permission.table_name == table)
3323 ._select(permission.record_id))
3324 return query
3325
3326 @staticmethod
3327 - def archive(form,
3328 archive_table=None,
3329 current_record='current_record',
3330 archive_current=False,
3331 fields=None):
3332 """
3333 If you have a table (db.mytable) that needs full revision history you can just do:
3334
3335 form=crud.update(db.mytable,myrecord,onaccept=auth.archive)
3336
3337 or
3338
3339 form=SQLFORM(db.mytable,myrecord).process(onaccept=auth.archive)
3340
3341 crud.archive will define a new table "mytable_archive" and store
3342 a copy of the current record (if archive_current=True)
3343 or a copy of the previous record (if archive_current=False)
3344 in the newly created table including a reference
3345 to the current record.
3346
3347 fields allows to specify extra fields that need to be archived.
3348
3349 If you want to access such table you need to define it yourself
3350 in a model:
3351
3352 db.define_table('mytable_archive',
3353 Field('current_record',db.mytable),
3354 db.mytable)
3355
3356 Notice such table includes all fields of db.mytable plus one: current_record.
3357 crud.archive does not timestamp the stored record unless your original table
3358 has a fields like:
3359
3360 db.define_table(...,
3361 Field('saved_on','datetime',
3362 default=request.now,update=request.now,writable=False),
3363 Field('saved_by',auth.user,
3364 default=auth.user_id,update=auth.user_id,writable=False),
3365
3366 there is nothing special about these fields since they are filled before
3367 the record is archived.
3368
3369 If you want to change the archive table name and the name of the reference field
3370 you can do, for example:
3371
3372 db.define_table('myhistory',
3373 Field('parent_record',db.mytable),
3374 db.mytable)
3375
3376 and use it as:
3377
3378 form=crud.update(db.mytable,myrecord,
3379 onaccept=lambda form:crud.archive(form,
3380 archive_table=db.myhistory,
3381 current_record='parent_record'))
3382
3383 """
3384 if not archive_current and not form.record:
3385 return None
3386 table = form.table
3387 if not archive_table:
3388 archive_table_name = '%s_archive' % table
3389 if not archive_table_name in table._db:
3390 table._db.define_table(
3391 archive_table_name,
3392 Field(current_record, table),
3393 *[field.clone(unique=False) for field in table])
3394 archive_table = table._db[archive_table_name]
3395 new_record = {current_record: form.vars.id}
3396 for fieldname in archive_table.fields:
3397 if not fieldname in ['id', current_record]:
3398 if archive_current and fieldname in form.vars:
3399 new_record[fieldname] = form.vars[fieldname]
3400 elif form.record and fieldname in form.record:
3401 new_record[fieldname] = form.record[fieldname]
3402 if fields:
3403 new_record.update(fields)
3404 id = archive_table.insert(**new_record)
3405 return id
3406
3407 - def wiki(self,
3408 slug=None,
3409 env=None,
3410 render='markmin',
3411 manage_permissions=False,
3412 force_prefix='',
3413 restrict_search=False,
3414 resolve=True,
3415 extra=None,
3416 menu_groups=None,
3417 templates=None,
3418 migrate=True,
3419 controller=None,
3420 function=None):
3421
3422 if controller and function: resolve = False
3423
3424 if not hasattr(self, '_wiki'):
3425 self._wiki = Wiki(self, render=render,
3426 manage_permissions=manage_permissions,
3427 force_prefix=force_prefix,
3428 restrict_search=restrict_search,
3429 env=env, extra=extra or {},
3430 menu_groups=menu_groups,
3431 templates=templates,
3432 migrate=migrate,
3433 controller=controller,
3434 function=function)
3435 else:
3436 self._wiki.env.update(env or {})
3437
3438
3439
3440 wiki = None
3441 if resolve:
3442 action = str(current.request.args(0)).startswith("_")
3443 if slug and not action:
3444 wiki = self._wiki.read(slug)
3445 if isinstance(wiki, dict) and wiki.has_key('content'):
3446
3447 wiki = wiki['content']
3448 else:
3449 wiki = self._wiki()
3450 if isinstance(wiki, basestring):
3451 wiki = XML(wiki)
3452 return wiki
3453
3455 """to be used in menu.py for app wide wiki menus"""
3456 if (hasattr(self, "_wiki") and
3457 self._wiki.settings.controller and
3458 self._wiki.settings.function):
3459 self._wiki.automenu()
3460
3461
3462 -class Crud(object):
3463
3464 - def url(self, f=None, args=None, vars=None):
3465 """
3466 this should point to the controller that exposes
3467 download and crud
3468 """
3469 if args is None:
3470 args = []
3471 if vars is None:
3472 vars = {}
3473 return URL(c=self.settings.controller, f=f, args=args, vars=vars)
3474
3475 - def __init__(self, environment, db=None, controller='default'):
3524
3526 args = current.request.args
3527 if len(args) < 1:
3528 raise HTTP(404)
3529 elif args[0] == 'tables':
3530 return self.tables()
3531 elif len(args) > 1 and not args(1) in self.db.tables:
3532 raise HTTP(404)
3533 table = self.db[args(1)]
3534 if args[0] == 'create':
3535 return self.create(table)
3536 elif args[0] == 'select':
3537 return self.select(table, linkto=self.url(args='read'))
3538 elif args[0] == 'search':
3539 form, rows = self.search(table, linkto=self.url(args='read'))
3540 return DIV(form, SQLTABLE(rows))
3541 elif args[0] == 'read':
3542 return self.read(table, args(2))
3543 elif args[0] == 'update':
3544 return self.update(table, args(2))
3545 elif args[0] == 'delete':
3546 return self.delete(table, args(2))
3547 else:
3548 raise HTTP(404)
3549
3553
3562
3567
3568 @staticmethod
3569 - def archive(form, archive_table=None, current_record='current_record'):
3570 return Auth.archive(form, archive_table=archive_table,
3571 current_record=current_record)
3572
3573 - def update(
3574 self,
3575 table,
3576 record,
3577 next=DEFAULT,
3578 onvalidation=DEFAULT,
3579 onaccept=DEFAULT,
3580 ondelete=DEFAULT,
3581 log=DEFAULT,
3582 message=DEFAULT,
3583 deletable=DEFAULT,
3584 formname=DEFAULT,
3585 **attributes
3586 ):
3587 """
3588 method: Crud.update(table, record, [next=DEFAULT
3589 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT
3590 [, message=DEFAULT[, deletable=DEFAULT]]]]]])
3591
3592 """
3593 if not (isinstance(table, self.db.Table) or table in self.db.tables) \
3594 or (isinstance(record, str) and not str(record).isdigit()):
3595 raise HTTP(404)
3596 if not isinstance(table, self.db.Table):
3597 table = self.db[table]
3598 try:
3599 record_id = record.id
3600 except:
3601 record_id = record or 0
3602 if record_id and not self.has_permission('update', table, record_id):
3603 redirect(self.settings.auth.settings.on_failed_authorization)
3604 if not record_id and not self.has_permission('create', table, record_id):
3605 redirect(self.settings.auth.settings.on_failed_authorization)
3606
3607 request = current.request
3608 response = current.response
3609 session = current.session
3610 if request.extension == 'json' and request.vars.json:
3611 request.vars.update(json_parser.loads(request.vars.json))
3612 if next is DEFAULT:
3613 next = request.get_vars._next \
3614 or request.post_vars._next \
3615 or self.settings.update_next
3616 if onvalidation is DEFAULT:
3617 onvalidation = self.settings.update_onvalidation
3618 if onaccept is DEFAULT:
3619 onaccept = self.settings.update_onaccept
3620 if ondelete is DEFAULT:
3621 ondelete = self.settings.update_ondelete
3622 if log is DEFAULT:
3623 log = self.messages.update_log
3624 if deletable is DEFAULT:
3625 deletable = self.settings.update_deletable
3626 if message is DEFAULT:
3627 message = self.messages.record_updated
3628 if not 'hidden' in attributes:
3629 attributes['hidden'] = {}
3630 attributes['hidden']['_next'] = next
3631 form = SQLFORM(
3632 table,
3633 record,
3634 showid=self.settings.showid,
3635 submit_button=self.messages.submit_button,
3636 delete_label=self.messages.delete_label,
3637 deletable=deletable,
3638 upload=self.settings.download_url,
3639 formstyle=self.settings.formstyle,
3640 separator=self.settings.label_separator,
3641 **attributes
3642 )
3643 self.accepted = False
3644 self.deleted = False
3645 captcha = self.settings.update_captcha or self.settings.captcha
3646 if record and captcha:
3647 addrow(form, captcha.label, captcha, captcha.comment,
3648 self.settings.formstyle, 'captcha__row')
3649 captcha = self.settings.create_captcha or self.settings.captcha
3650 if not record and captcha:
3651 addrow(form, captcha.label, captcha, captcha.comment,
3652 self.settings.formstyle, 'captcha__row')
3653 if not request.extension in ('html', 'load'):
3654 (_session, _formname) = (None, None)
3655 else:
3656 (_session, _formname) = (
3657 session, '%s/%s' % (table._tablename, form.record_id))
3658 if not formname is DEFAULT:
3659 _formname = formname
3660 keepvalues = self.settings.keepvalues
3661 if request.vars.delete_this_record:
3662 keepvalues = False
3663 if isinstance(onvalidation, StorageList):
3664 onvalidation = onvalidation.get(table._tablename, [])
3665 if form.accepts(request, _session, formname=_formname,
3666 onvalidation=onvalidation, keepvalues=keepvalues,
3667 hideerror=self.settings.hideerror,
3668 detect_record_change=self.settings.detect_record_change):
3669 self.accepted = True
3670 response.flash = message
3671 if log:
3672 self.log_event(log, form.vars)
3673 if request.vars.delete_this_record:
3674 self.deleted = True
3675 message = self.messages.record_deleted
3676 callback(ondelete, form, table._tablename)
3677 response.flash = message
3678 callback(onaccept, form, table._tablename)
3679 if not request.extension in ('html', 'load'):
3680 raise HTTP(200, 'RECORD CREATED/UPDATED')
3681 if isinstance(next, (list, tuple)):
3682 next = next[0]
3683 if next:
3684 next = replace_id(next, form)
3685 session.flash = response.flash
3686 redirect(next)
3687 elif not request.extension in ('html', 'load'):
3688 raise HTTP(401, serializers.json(dict(errors=form.errors)))
3689 return form
3690
3702 """
3703 method: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT
3704 [, onaccept=DEFAULT [, log=DEFAULT[, message=DEFAULT]]]]])
3705 """
3706
3707 if next is DEFAULT:
3708 next = self.settings.create_next
3709 if onvalidation is DEFAULT:
3710 onvalidation = self.settings.create_onvalidation
3711 if onaccept is DEFAULT:
3712 onaccept = self.settings.create_onaccept
3713 if log is DEFAULT:
3714 log = self.messages.create_log
3715 if message is DEFAULT:
3716 message = self.messages.record_created
3717 return self.update(
3718 table,
3719 None,
3720 next=next,
3721 onvalidation=onvalidation,
3722 onaccept=onaccept,
3723 log=log,
3724 message=message,
3725 deletable=False,
3726 formname=formname,
3727 **attributes
3728 )
3729
3730 - def read(self, table, record):
3731 if not (isinstance(table, self.db.Table) or table in self.db.tables) \
3732 or (isinstance(record, str) and not str(record).isdigit()):
3733 raise HTTP(404)
3734 if not isinstance(table, self.db.Table):
3735 table = self.db[table]
3736 if not self.has_permission('read', table, record):
3737 redirect(self.settings.auth.settings.on_failed_authorization)
3738 form = SQLFORM(
3739 table,
3740 record,
3741 readonly=True,
3742 comments=False,
3743 upload=self.settings.download_url,
3744 showid=self.settings.showid,
3745 formstyle=self.settings.formstyle,
3746 separator=self.settings.label_separator
3747 )
3748 if not current.request.extension in ('html', 'load'):
3749 return table._filter_fields(form.record, id=True)
3750 return form
3751
3752 - def delete(
3753 self,
3754 table,
3755 record_id,
3756 next=DEFAULT,
3757 message=DEFAULT,
3758 ):
3759 """
3760 method: Crud.delete(table, record_id, [next=DEFAULT
3761 [, message=DEFAULT]])
3762 """
3763 if not (isinstance(table, self.db.Table) or table in self.db.tables):
3764 raise HTTP(404)
3765 if not isinstance(table, self.db.Table):
3766 table = self.db[table]
3767 if not self.has_permission('delete', table, record_id):
3768 redirect(self.settings.auth.settings.on_failed_authorization)
3769 request = current.request
3770 session = current.session
3771 if next is DEFAULT:
3772 next = request.get_vars._next \
3773 or request.post_vars._next \
3774 or self.settings.delete_next
3775 if message is DEFAULT:
3776 message = self.messages.record_deleted
3777 record = table[record_id]
3778 if record:
3779 callback(self.settings.delete_onvalidation, record)
3780 del table[record_id]
3781 callback(self.settings.delete_onaccept, record, table._tablename)
3782 session.flash = message
3783 redirect(next)
3784
3785 - def rows(
3786 self,
3787 table,
3788 query=None,
3789 fields=None,
3790 orderby=None,
3791 limitby=None,
3792 ):
3793 if not (isinstance(table, self.db.Table) or table in self.db.tables):
3794 raise HTTP(404)
3795 if not self.has_permission('select', table):
3796 redirect(self.settings.auth.settings.on_failed_authorization)
3797
3798
3799 if not isinstance(table, self.db.Table):
3800 table = self.db[table]
3801 if not query:
3802 query = table.id > 0
3803 if not fields:
3804 fields = [field for field in table if field.readable]
3805 else:
3806 fields = [table[f] if isinstance(f, str) else f for f in fields]
3807 rows = self.db(query).select(*fields, **dict(orderby=orderby,
3808 limitby=limitby))
3809 return rows
3810
3811 - def select(
3812 self,
3813 table,
3814 query=None,
3815 fields=None,
3816 orderby=None,
3817 limitby=None,
3818 headers=None,
3819 **attr
3820 ):
3821 headers = headers or {}
3822 rows = self.rows(table, query, fields, orderby, limitby)
3823 if not rows:
3824 return None
3825 if not 'upload' in attr:
3826 attr['upload'] = self.url('download')
3827 if not current.request.extension in ('html', 'load'):
3828 return rows.as_list()
3829 if not headers:
3830 if isinstance(table, str):
3831 table = self.db[table]
3832 headers = dict((str(k), k.label) for k in table)
3833 return SQLTABLE(rows, headers=headers, **attr)
3834
3841
3842 - def get_query(self, field, op, value, refsearch=False):
3843 try:
3844 if refsearch:
3845 format = self.get_format(field)
3846 if op == 'equals':
3847 if not refsearch:
3848 return field == value
3849 else:
3850 return lambda row: row[field.name][format] == value
3851 elif op == 'not equal':
3852 if not refsearch:
3853 return field != value
3854 else:
3855 return lambda row: row[field.name][format] != value
3856 elif op == 'greater than':
3857 if not refsearch:
3858 return field > value
3859 else:
3860 return lambda row: row[field.name][format] > value
3861 elif op == 'less than':
3862 if not refsearch:
3863 return field < value
3864 else:
3865 return lambda row: row[field.name][format] < value
3866 elif op == 'starts with':
3867 if not refsearch:
3868 return field.like(value + '%')
3869 else:
3870 return lambda row: str(row[field.name][format]).startswith(value)
3871 elif op == 'ends with':
3872 if not refsearch:
3873 return field.like('%' + value)
3874 else:
3875 return lambda row: str(row[field.name][format]).endswith(value)
3876 elif op == 'contains':
3877 if not refsearch:
3878 return field.like('%' + value + '%')
3879 else:
3880 return lambda row: value in row[field.name][format]
3881 except:
3882 return None
3883
3884 - def search(self, *tables, **args):
3885 """
3886 Creates a search form and its results for a table
3887 Example usage:
3888 form, results = crud.search(db.test,
3889 queries = ['equals', 'not equal', 'contains'],
3890 query_labels={'equals':'Equals',
3891 'not equal':'Not equal'},
3892 fields = ['id','children'],
3893 field_labels = {
3894 'id':'ID','children':'Children'},
3895 zero='Please choose',
3896 query = (db.test.id > 0)&(db.test.id != 3) )
3897 """
3898 table = tables[0]
3899 fields = args.get('fields', table.fields)
3900 request = current.request
3901 db = self.db
3902 if not (isinstance(table, db.Table) or table in db.tables):
3903 raise HTTP(404)
3904 attributes = {}
3905 for key in ('orderby', 'groupby', 'left', 'distinct', 'limitby', 'cache'):
3906 if key in args:
3907 attributes[key] = args[key]
3908 tbl = TABLE()
3909 selected = []
3910 refsearch = []
3911 results = []
3912 showall = args.get('showall', False)
3913 if showall:
3914 selected = fields
3915 chkall = args.get('chkall', False)
3916 if chkall:
3917 for f in fields:
3918 request.vars['chk%s' % f] = 'on'
3919 ops = args.get('queries', [])
3920 zero = args.get('zero', '')
3921 if not ops:
3922 ops = ['equals', 'not equal', 'greater than',
3923 'less than', 'starts with',
3924 'ends with', 'contains']
3925 ops.insert(0, zero)
3926 query_labels = args.get('query_labels', {})
3927 query = args.get('query', table.id > 0)
3928 field_labels = args.get('field_labels', {})
3929 for field in fields:
3930 field = table[field]
3931 if not field.readable:
3932 continue
3933 fieldname = field.name
3934 chkval = request.vars.get('chk' + fieldname, None)
3935 txtval = request.vars.get('txt' + fieldname, None)
3936 opval = request.vars.get('op' + fieldname, None)
3937 row = TR(TD(INPUT(_type="checkbox", _name="chk" + fieldname,
3938 _disabled=(field.type == 'id'),
3939 value=(field.type == 'id' or chkval == 'on'))),
3940 TD(field_labels.get(fieldname, field.label)),
3941 TD(SELECT([OPTION(query_labels.get(op, op),
3942 _value=op) for op in ops],
3943 _name="op" + fieldname,
3944 value=opval)),
3945 TD(INPUT(_type="text", _name="txt" + fieldname,
3946 _value=txtval, _id='txt' + fieldname,
3947 _class=str(field.type))))
3948 tbl.append(row)
3949 if request.post_vars and (chkval or field.type == 'id'):
3950 if txtval and opval != '':
3951 if field.type[0:10] == 'reference ':
3952 refsearch.append(self.get_query(field,
3953 opval, txtval, refsearch=True))
3954 else:
3955 value, error = field.validate(txtval)
3956 if not error:
3957
3958 query &= self.get_query(field, opval, value)
3959 else:
3960 row[3].append(DIV(error, _class='error'))
3961 selected.append(field)
3962 form = FORM(tbl, INPUT(_type="submit"))
3963 if selected:
3964 try:
3965 results = db(query).select(*selected, **attributes)
3966 for r in refsearch:
3967 results = results.find(r)
3968 except:
3969 results = None
3970 return form, results
3971
3972
3973 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor()))
3974
3975
3976 -def fetch(url, data=None, headers=None,
3977 cookie=Cookie.SimpleCookie(),
3978 user_agent='Mozilla/5.0'):
3979 headers = headers or {}
3980 if not data is None:
3981 data = urllib.urlencode(data)
3982 if user_agent:
3983 headers['User-agent'] = user_agent
3984 headers['Cookie'] = ' '.join(
3985 ['%s=%s;' % (c.key, c.value) for c in cookie.values()])
3986 try:
3987 from google.appengine.api import urlfetch
3988 except ImportError:
3989 req = urllib2.Request(url, data, headers)
3990 html = urllib2.urlopen(req).read()
3991 else:
3992 method = ((data is None) and urlfetch.GET) or urlfetch.POST
3993 while url is not None:
3994 response = urlfetch.fetch(url=url, payload=data,
3995 method=method, headers=headers,
3996 allow_truncated=False, follow_redirects=False,
3997 deadline=10)
3998
3999 data = None
4000 method = urlfetch.GET
4001
4002 cookie.load(response.headers.get('set-cookie', ''))
4003 url = response.headers.get('location')
4004 html = response.content
4005 return html
4006
4007 regex_geocode = \
4008 re.compile(r"""<geometry>[\W]*?<location>[\W]*?<lat>(?P<la>[^<]*)</lat>[\W]*?<lng>(?P<lo>[^<]*)</lng>[\W]*?</location>""")
4012 try:
4013 a = urllib.quote(address)
4014 txt = fetch('http://maps.googleapis.com/maps/api/geocode/xml?sensor=false&address=%s'
4015 % a)
4016 item = regex_geocode.search(txt)
4017 (la, lo) = (float(item.group('la')), float(item.group('lo')))
4018 return (la, lo)
4019 except:
4020 return (0.0, 0.0)
4021
4024 c = f.func_code.co_argcount
4025 n = f.func_code.co_varnames[:c]
4026
4027 defaults = f.func_defaults or []
4028 pos_args = n[0:-len(defaults)]
4029 named_args = n[-len(defaults):]
4030
4031 arg_dict = {}
4032
4033
4034 for pos_index, pos_val in enumerate(a[:c]):
4035 arg_dict[n[pos_index]
4036 ] = pos_val
4037
4038
4039
4040 for arg_name in pos_args[len(arg_dict):]:
4041 if arg_name in b:
4042 arg_dict[arg_name] = b[arg_name]
4043
4044 if len(arg_dict) >= len(pos_args):
4045
4046
4047 for arg_name in named_args:
4048 if arg_name in b:
4049 arg_dict[arg_name] = b[arg_name]
4050
4051 return f(**arg_dict)
4052
4053
4054 raise HTTP(404, "Object does not exist")
4055
4058
4060 self.run_procedures = {}
4061 self.csv_procedures = {}
4062 self.xml_procedures = {}
4063 self.rss_procedures = {}
4064 self.json_procedures = {}
4065 self.jsonrpc_procedures = {}
4066 self.jsonrpc2_procedures = {}
4067 self.xmlrpc_procedures = {}
4068 self.amfrpc_procedures = {}
4069 self.amfrpc3_procedures = {}
4070 self.soap_procedures = {}
4071
4073 """
4074 example:
4075
4076 service = Service()
4077 @service.run
4078 def myfunction(a, b):
4079 return a + b
4080 def call():
4081 return service()
4082
4083 Then call it with:
4084
4085 wget http://..../app/default/call/run/myfunction?a=3&b=4
4086
4087 """
4088 self.run_procedures[f.__name__] = f
4089 return f
4090
4092 """
4093 example:
4094
4095 service = Service()
4096 @service.csv
4097 def myfunction(a, b):
4098 return a + b
4099 def call():
4100 return service()
4101
4102 Then call it with:
4103
4104 wget http://..../app/default/call/csv/myfunction?a=3&b=4
4105
4106 """
4107 self.run_procedures[f.__name__] = f
4108 return f
4109
4111 """
4112 example:
4113
4114 service = Service()
4115 @service.xml
4116 def myfunction(a, b):
4117 return a + b
4118 def call():
4119 return service()
4120
4121 Then call it with:
4122
4123 wget http://..../app/default/call/xml/myfunction?a=3&b=4
4124
4125 """
4126 self.run_procedures[f.__name__] = f
4127 return f
4128
4130 """
4131 example:
4132
4133 service = Service()
4134 @service.rss
4135 def myfunction():
4136 return dict(title=..., link=..., description=...,
4137 created_on=..., entries=[dict(title=..., link=...,
4138 description=..., created_on=...])
4139 def call():
4140 return service()
4141
4142 Then call it with:
4143
4144 wget http://..../app/default/call/rss/myfunction
4145
4146 """
4147 self.rss_procedures[f.__name__] = f
4148 return f
4149
4150 - def json(self, f):
4151 """
4152 example:
4153
4154 service = Service()
4155 @service.json
4156 def myfunction(a, b):
4157 return [{a: b}]
4158 def call():
4159 return service()
4160
4161 Then call it with:
4162
4163 wget http://..../app/default/call/json/myfunction?a=hello&b=world
4164
4165 """
4166 self.json_procedures[f.__name__] = f
4167 return f
4168
4170 """
4171 example:
4172
4173 service = Service()
4174 @service.jsonrpc
4175 def myfunction(a, b):
4176 return a + b
4177 def call():
4178 return service()
4179
4180 Then call it with:
4181
4182 wget http://..../app/default/call/jsonrpc/myfunction?a=hello&b=world
4183
4184 """
4185 self.jsonrpc_procedures[f.__name__] = f
4186 return f
4187
4189 """
4190 example:
4191
4192 service = Service()
4193 @service.jsonrpc2
4194 def myfunction(a, b):
4195 return a + b
4196 def call():
4197 return service()
4198
4199 Then call it with:
4200
4201 wget --post-data '{"jsonrpc": "2.0", "id": 1, "method": "myfunction", "params": {"a": 1, "b": 2}}' http://..../app/default/call/jsonrpc2
4202
4203 """
4204 self.jsonrpc2_procedures[f.__name__] = f
4205 return f
4206
4208 """
4209 example:
4210
4211 service = Service()
4212 @service.xmlrpc
4213 def myfunction(a, b):
4214 return a + b
4215 def call():
4216 return service()
4217
4218 The call it with:
4219
4220 wget http://..../app/default/call/xmlrpc/myfunction?a=hello&b=world
4221
4222 """
4223 self.xmlrpc_procedures[f.__name__] = f
4224 return f
4225
4227 """
4228 example:
4229
4230 service = Service()
4231 @service.amfrpc
4232 def myfunction(a, b):
4233 return a + b
4234 def call():
4235 return service()
4236
4237 The call it with:
4238
4239 wget http://..../app/default/call/amfrpc/myfunction?a=hello&b=world
4240
4241 """
4242 self.amfrpc_procedures[f.__name__] = f
4243 return f
4244
4245 - def amfrpc3(self, domain='default'):
4246 """
4247 example:
4248
4249 service = Service()
4250 @service.amfrpc3('domain')
4251 def myfunction(a, b):
4252 return a + b
4253 def call():
4254 return service()
4255
4256 The call it with:
4257
4258 wget http://..../app/default/call/amfrpc3/myfunction?a=hello&b=world
4259
4260 """
4261 if not isinstance(domain, str):
4262 raise SyntaxError("AMF3 requires a domain for function")
4263
4264 def _amfrpc3(f):
4265 if domain:
4266 self.amfrpc3_procedures[domain + '.' + f.__name__] = f
4267 else:
4268 self.amfrpc3_procedures[f.__name__] = f
4269 return f
4270 return _amfrpc3
4271
4272 - def soap(self, name=None, returns=None, args=None, doc=None):
4273 """
4274 example:
4275
4276 service = Service()
4277 @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,})
4278 def myfunction(a, b):
4279 return a + b
4280 def call():
4281 return service()
4282
4283 The call it with:
4284
4285 from gluon.contrib.pysimplesoap.client import SoapClient
4286 client = SoapClient(wsdl="http://..../app/default/call/soap?WSDL")
4287 response = client.MyFunction(a=1,b=2)
4288 return response['result']
4289
4290 Exposes online generated documentation and xml example messages at:
4291 - http://..../app/default/call/soap
4292 """
4293
4294 def _soap(f):
4295 self.soap_procedures[name or f.__name__] = f, returns, args, doc
4296 return f
4297 return _soap
4298
4307
4309 request = current.request
4310 response = current.response
4311 response.headers['Content-Type'] = 'text/x-csv'
4312 if not args:
4313 args = request.args
4314
4315 def none_exception(value):
4316 if isinstance(value, unicode):
4317 return value.encode('utf8')
4318 if hasattr(value, 'isoformat'):
4319 return value.isoformat()[:19].replace('T', ' ')
4320 if value is None:
4321 return '<NULL>'
4322 return value
4323 if args and args[0] in self.run_procedures:
4324 import types
4325 r = universal_caller(self.run_procedures[args[0]],
4326 *args[1:], **dict(request.vars))
4327 s = cStringIO.StringIO()
4328 if hasattr(r, 'export_to_csv_file'):
4329 r.export_to_csv_file(s)
4330 elif r and not isinstance(r, types.GeneratorType) and isinstance(r[0], (dict, Storage)):
4331 import csv
4332 writer = csv.writer(s)
4333 writer.writerow(r[0].keys())
4334 for line in r:
4335 writer.writerow([none_exception(v)
4336 for v in line.values()])
4337 else:
4338 import csv
4339 writer = csv.writer(s)
4340 for line in r:
4341 writer.writerow(line)
4342 return s.getvalue()
4343 self.error()
4344
4358
4371
4385
4388 jrpc_error = Service.jsonrpc_errors.get(code)
4389 if jrpc_error:
4390 self.message, self.description = jrpc_error
4391 self.code, self.info = code, info
4392
4393
4394 jsonrpc_errors = {
4395 -32700: ("Parse error. Invalid JSON was received by the server.", "An error occurred on the server while parsing the JSON text."),
4396 -32600: ("Invalid Request", "The JSON sent is not a valid Request object."),
4397 -32601: ("Method not found", "The method does not exist / is not available."),
4398 -32602: ("Invalid params", "Invalid method parameter(s)."),
4399 -32603: ("Internal error", "Internal JSON-RPC error."),
4400 -32099: ("Server error", "Reserved for implementation-defined server-errors.")}
4401
4402
4404 def return_response(id, result):
4405 return serializers.json({'version': '1.1',
4406 'id': id, 'result': result, 'error': None})
4407
4408 def return_error(id, code, message, data=None):
4409 error = {'name': 'JSONRPCError',
4410 'code': code, 'message': message}
4411 if data is not None:
4412 error['data'] = data
4413 return serializers.json({'id': id,
4414 'version': '1.1',
4415 'error': error,
4416 })
4417
4418 request = current.request
4419 response = current.response
4420 response.headers['Content-Type'] = 'application/json; charset=utf-8'
4421 methods = self.jsonrpc_procedures
4422 data = json_parser.loads(request.body.read())
4423 jsonrpc_2 = data.get('jsonrpc')
4424 if jsonrpc_2:
4425 return self.serve_jsonrpc2(data)
4426 id, method, params = data['id'], data['method'], data.get('params', '')
4427 if not method in methods:
4428 return return_error(id, 100, 'method "%s" does not exist' % method)
4429 try:
4430 if isinstance(params,dict):
4431 s = methods[method](**params)
4432 else:
4433 s = methods[method](*params)
4434 if hasattr(s, 'as_list'):
4435 s = s.as_list()
4436 return return_response(id, s)
4437 except Service.JsonRpcException, e:
4438 return return_error(id, e.code, e.info)
4439 except BaseException:
4440 etype, eval, etb = sys.exc_info()
4441 code = 100
4442 message = '%s: %s' % (etype.__name__, eval)
4443 data = request.is_local and traceback.format_tb(etb)
4444 return return_error(id, code, message, data)
4445 except:
4446 etype, eval, etb = sys.exc_info()
4447 return return_error(id, 100, 'Exception %s: %s' % (etype, eval))
4448
4450
4451 def return_response(id, result):
4452 if not must_respond:
4453 return None
4454 return serializers.json({'jsonrpc': '2.0',
4455 'id': id, 'result': result})
4456
4457 def return_error(id, code, message=None, data=None):
4458 error = {'code': code}
4459 if message is None:
4460 error['message'] = Service.jsonrpc_errors[code][0]
4461 else:
4462 error['message'] = message
4463 if data is None:
4464 error['data'] = Service.jsonrpc_errors[code][1]
4465 else:
4466 error['data'] = data
4467 return serializers.json({'jsonrpc': '2.0',
4468 'id': id,
4469 'error': error})
4470
4471 def validate(data):
4472 """
4473 Validate request as defined in: http://www.jsonrpc.org/specification#request_object.
4474
4475 :param data: The json object.
4476 :type name: str.
4477
4478 :returns:
4479 - True -- if successful
4480 - False -- if no error should be reported (i.e. data is missing 'id' member)
4481
4482 :raises: JsonRPCException
4483
4484 """
4485
4486 iparms = set(data.keys())
4487 mandatory_args = set(['jsonrpc', 'method'])
4488 missing_args = mandatory_args - iparms
4489
4490 if missing_args:
4491 raise Service.JsonRpcException(-32600, 'Missing arguments %s.' % list(missing_args))
4492 if data['jsonrpc'] != '2.0':
4493 raise Service.JsonRpcException(-32603, 'Unsupported jsonrpc version "%s"' % data['jsonrpc'])
4494 if 'id' not in iparms:
4495 return False
4496
4497 return True
4498
4499
4500
4501 request = current.request
4502 response = current.response
4503 if not data:
4504 response.headers['Content-Type'] = 'application/json; charset=utf-8'
4505 try:
4506 data = json_parser.loads(request.body.read())
4507 except ValueError:
4508 return return_error(None, -32700)
4509 except json_parser.JSONDecodeError:
4510 return return_error(None, -32700)
4511
4512
4513 if isinstance(data, list) and not batch_element:
4514 retlist = []
4515 for c in data:
4516 retstr = self.serve_jsonrpc2(c, batch_element=True)
4517 if retstr:
4518 retlist.append(retstr)
4519 if len(retlist) == 0:
4520 return ''
4521 else:
4522 return "[" + ','.join(retlist) + "]"
4523 methods = self.jsonrpc2_procedures
4524 methods.update(self.jsonrpc_procedures)
4525
4526 try:
4527 must_respond = validate(data)
4528 except Service.JsonRpcException, e:
4529 return return_error(None, e.code, e.info)
4530
4531 id, method, params = data.get('id'), data['method'], data.get('params', '')
4532 if not method in methods:
4533 return return_error(id, -32601, data='Method "%s" does not exist' % method)
4534 try:
4535 if isinstance(params,dict):
4536 s = methods[method](**params)
4537 else:
4538 s = methods[method](*params)
4539 if hasattr(s, 'as_list'):
4540 s = s.as_list()
4541 if must_respond:
4542 return return_response(id, s)
4543 else:
4544 return ''
4545 except HTTP, e:
4546 raise e
4547 except Service.JsonRpcException, e:
4548 return return_error(id, e.code, e.info)
4549 except BaseException:
4550 etype, eval, etb = sys.exc_info()
4551 code = -32099
4552 data = '%s: %s\n' % (etype.__name__, eval) + str(request.is_local and traceback.format_tb(etb))
4553 return return_error(id, code, data=data)
4554 except:
4555 etype, eval, etb = sys.exc_info()
4556 return return_error(id, -32099, data='Exception %s: %s' % (etype, eval))
4557
4558
4564
4566 try:
4567 import pyamf
4568 import pyamf.remoting.gateway
4569 except:
4570 return "pyamf not installed or not in Python sys.path"
4571 request = current.request
4572 response = current.response
4573 if version == 3:
4574 services = self.amfrpc3_procedures
4575 base_gateway = pyamf.remoting.gateway.BaseGateway(services)
4576 pyamf_request = pyamf.remoting.decode(request.body)
4577 else:
4578 services = self.amfrpc_procedures
4579 base_gateway = pyamf.remoting.gateway.BaseGateway(services)
4580 context = pyamf.get_context(pyamf.AMF0)
4581 pyamf_request = pyamf.remoting.decode(request.body, context)
4582 pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion)
4583 for name, message in pyamf_request:
4584 pyamf_response[name] = base_gateway.getProcessor(message)(message)
4585 response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE
4586 if version == 3:
4587 return pyamf.remoting.encode(pyamf_response).getvalue()
4588 else:
4589 return pyamf.remoting.encode(pyamf_response, context).getvalue()
4590
4592 try:
4593 from contrib.pysimplesoap.server import SoapDispatcher
4594 except:
4595 return "pysimplesoap not installed in contrib"
4596 request = current.request
4597 response = current.response
4598 procedures = self.soap_procedures
4599
4600 location = "%s://%s%s" % (
4601 request.env.wsgi_url_scheme,
4602 request.env.http_host,
4603 URL(r=request, f="call/soap", vars={}))
4604 namespace = 'namespace' in response and response.namespace or location
4605 documentation = response.description or ''
4606 dispatcher = SoapDispatcher(
4607 name=response.title,
4608 location=location,
4609 action=location,
4610 namespace=namespace,
4611 prefix='pys',
4612 documentation=documentation,
4613 ns=True)
4614 for method, (function, returns, args, doc) in procedures.iteritems():
4615 dispatcher.register_function(method, function, returns, args, doc)
4616 if request.env.request_method == 'POST':
4617
4618 response.headers['Content-Type'] = 'text/xml'
4619 return dispatcher.dispatch(request.body.read())
4620 elif 'WSDL' in request.vars:
4621
4622 response.headers['Content-Type'] = 'text/xml'
4623 return dispatcher.wsdl()
4624 elif 'op' in request.vars:
4625
4626 response.headers['Content-Type'] = 'text/html'
4627 method = request.vars['op']
4628 sample_req_xml, sample_res_xml, doc = dispatcher.help(method)
4629 body = [H1("Welcome to Web2Py SOAP webservice gateway"),
4630 A("See all webservice operations",
4631 _href=URL(r=request, f="call/soap", vars={})),
4632 H2(method),
4633 P(doc),
4634 UL(LI("Location: %s" % dispatcher.location),
4635 LI("Namespace: %s" % dispatcher.namespace),
4636 LI("SoapAction: %s" % dispatcher.action),
4637 ),
4638 H3("Sample SOAP XML Request Message:"),
4639 CODE(sample_req_xml, language="xml"),
4640 H3("Sample SOAP XML Response Message:"),
4641 CODE(sample_res_xml, language="xml"),
4642 ]
4643 return {'body': body}
4644 else:
4645
4646 response.headers['Content-Type'] = 'text/html'
4647 body = [H1("Welcome to Web2Py SOAP webservice gateway"),
4648 P(response.description),
4649 P("The following operations are available"),
4650 A("See WSDL for webservice description",
4651 _href=URL(r=request, f="call/soap", vars={"WSDL":None})),
4652 UL([LI(A("%s: %s" % (method, doc or ''),
4653 _href=URL(r=request, f="call/soap", vars={'op': method})))
4654 for method, doc in dispatcher.list_methods()]),
4655 ]
4656 return {'body': body}
4657
4659 """
4660 register services with:
4661 service = Service()
4662 @service.run
4663 @service.rss
4664 @service.json
4665 @service.jsonrpc
4666 @service.xmlrpc
4667 @service.amfrpc
4668 @service.amfrpc3('domain')
4669 @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,})
4670
4671 expose services with
4672
4673 def call(): return service()
4674
4675 call services with
4676 http://..../app/default/call/run?[parameters]
4677 http://..../app/default/call/rss?[parameters]
4678 http://..../app/default/call/json?[parameters]
4679 http://..../app/default/call/jsonrpc
4680 http://..../app/default/call/xmlrpc
4681 http://..../app/default/call/amfrpc
4682 http://..../app/default/call/amfrpc3
4683 http://..../app/default/call/soap
4684 """
4685
4686 request = current.request
4687 if len(request.args) < 1:
4688 raise HTTP(404, "Not Found")
4689 arg0 = request.args(0)
4690 if arg0 == 'run':
4691 return self.serve_run(request.args[1:])
4692 elif arg0 == 'rss':
4693 return self.serve_rss(request.args[1:])
4694 elif arg0 == 'csv':
4695 return self.serve_csv(request.args[1:])
4696 elif arg0 == 'xml':
4697 return self.serve_xml(request.args[1:])
4698 elif arg0 == 'json':
4699 return self.serve_json(request.args[1:])
4700 elif arg0 == 'jsonrpc':
4701 return self.serve_jsonrpc()
4702 elif arg0 == 'jsonrpc2':
4703 return self.serve_jsonrpc2()
4704 elif arg0 == 'xmlrpc':
4705 return self.serve_xmlrpc()
4706 elif arg0 == 'amfrpc':
4707 return self.serve_amfrpc()
4708 elif arg0 == 'amfrpc3':
4709 return self.serve_amfrpc(3)
4710 elif arg0 == 'soap':
4711 return self.serve_soap()
4712 else:
4713 self.error()
4714
4716 raise HTTP(404, "Object does not exist")
4717
4720 """
4721 Executes a task on completion of the called action. For example:
4722
4723 from gluon.tools import completion
4724 @completion(lambda d: logging.info(repr(d)))
4725 def index():
4726 return dict(message='hello')
4727
4728 It logs the output of the function every time input is called.
4729 The argument of completion is executed in a new thread.
4730 """
4731 def _completion(f):
4732 def __completion(*a, **b):
4733 d = None
4734 try:
4735 d = f(*a, **b)
4736 return d
4737 finally:
4738 thread.start_new_thread(callback, (d,))
4739 return __completion
4740 return _completion
4741
4744 if isinstance(d, datetime.datetime):
4745 dt = datetime.datetime.now() - d
4746 elif isinstance(d, datetime.date):
4747 dt = datetime.date.today() - d
4748 elif not d:
4749 return ''
4750 else:
4751 return '[invalid date]'
4752 if dt.days < 0:
4753 suffix = ' from now'
4754 dt = -dt
4755 else:
4756 suffix = ' ago'
4757 if dt.days >= 2 * 365:
4758 return T('%d years' + suffix) % int(dt.days / 365)
4759 elif dt.days >= 365:
4760 return T('1 year' + suffix)
4761 elif dt.days >= 60:
4762 return T('%d months' + suffix) % int(dt.days / 30)
4763 elif dt.days > 21:
4764 return T('1 month' + suffix)
4765 elif dt.days >= 14:
4766 return T('%d weeks' + suffix) % int(dt.days / 7)
4767 elif dt.days >= 7:
4768 return T('1 week' + suffix)
4769 elif dt.days > 1:
4770 return T('%d days' + suffix) % dt.days
4771 elif dt.days == 1:
4772 return T('1 day' + suffix)
4773 elif dt.seconds >= 2 * 60 * 60:
4774 return T('%d hours' + suffix) % int(dt.seconds / 3600)
4775 elif dt.seconds >= 60 * 60:
4776 return T('1 hour' + suffix)
4777 elif dt.seconds >= 2 * 60:
4778 return T('%d minutes' + suffix) % int(dt.seconds / 60)
4779 elif dt.seconds >= 60:
4780 return T('1 minute' + suffix)
4781 elif dt.seconds > 1:
4782 return T('%d seconds' + suffix) % dt.seconds
4783 elif dt.seconds == 1:
4784 return T('1 second' + suffix)
4785 else:
4786 return T('now')
4787
4797 lock1 = thread.allocate_lock()
4798 lock2 = thread.allocate_lock()
4799 lock1.acquire()
4800 thread.start_new_thread(f, ())
4801 a = PluginManager()
4802 a.x = 5
4803 lock1.release()
4804 lock2.acquire()
4805 return a.x
4806
4809 """
4810
4811 Plugin Manager is similar to a storage object but it is a single level singleton
4812 this means that multiple instances within the same thread share the same attributes
4813 Its constructor is also special. The first argument is the name of the plugin you are defining.
4814 The named arguments are parameters needed by the plugin with default values.
4815 If the parameters were previous defined, the old values are used.
4816
4817 For example:
4818
4819 ### in some general configuration file:
4820 >>> plugins = PluginManager()
4821 >>> plugins.me.param1=3
4822
4823 ### within the plugin model
4824 >>> _ = PluginManager('me',param1=5,param2=6,param3=7)
4825
4826 ### where the plugin is used
4827 >>> print plugins.me.param1
4828 3
4829 >>> print plugins.me.param2
4830 6
4831 >>> plugins.me.param3 = 8
4832 >>> print plugins.me.param3
4833 8
4834
4835 Here are some tests:
4836
4837 >>> a=PluginManager()
4838 >>> a.x=6
4839 >>> b=PluginManager('check')
4840 >>> print b.x
4841 6
4842 >>> b=PluginManager() # reset settings
4843 >>> print b.x
4844 <Storage {}>
4845 >>> b.x=7
4846 >>> print a.x
4847 7
4848 >>> a.y.z=8
4849 >>> print b.y.z
4850 8
4851 >>> test_thread_separation()
4852 5
4853 >>> plugins=PluginManager('me',db='mydb')
4854 >>> print plugins.me.db
4855 mydb
4856 >>> print 'me' in plugins
4857 True
4858 >>> print plugins.me.installed
4859 True
4860 """
4861 instances = {}
4862
4864 id = thread.get_ident()
4865 lock = thread.allocate_lock()
4866 try:
4867 lock.acquire()
4868 try:
4869 return cls.instances[id]
4870 except KeyError:
4871 instance = object.__new__(cls, *a, **b)
4872 cls.instances[id] = instance
4873 return instance
4874 finally:
4875 lock.release()
4876
4877 - def __init__(self, plugin=None, **defaults):
4884
4886 if not key in self.__dict__:
4887 self.__dict__[key] = Storage()
4888 return self.__dict__[key]
4889
4891 return self.__dict__.keys()
4892
4894 return key in self.__dict__
4895
4898 - def __init__(self, base=None, basename=None, extensions=None, allow_download=True):
4899 """
4900 Usage:
4901
4902 def static():
4903 return dict(files=Expose())
4904
4905 or
4906
4907 def static():
4908 path = os.path.join(request.folder,'static','public')
4909 return dict(files=Expose(path,basename='public'))
4910
4911 extensions:
4912 an optional list of file extensions for filtering displayed files:
4913 ['.py', '.jpg']
4914 allow_download: whether to allow downloading selected files
4915 """
4916 current.session.forget()
4917 base = base or os.path.join(current.request.folder, 'static')
4918 basename = basename or current.request.function
4919 self.basename = basename
4920 self.args = current.request.raw_args and \
4921 [arg for arg in current.request.raw_args.split('/') if arg] or []
4922 filename = os.path.join(base, *self.args)
4923 if not os.path.exists(filename):
4924 raise HTTP(404, "FILE NOT FOUND")
4925 if not os.path.normpath(filename).startswith(base):
4926 raise HTTP(401, "NOT AUTHORIZED")
4927 if allow_download and not os.path.isdir(filename):
4928 current.response.headers['Content-Type'] = contenttype(filename)
4929 raise HTTP(200, open(filename, 'rb'), **current.response.headers)
4930 self.path = path = os.path.join(filename, '*')
4931 self.folders = [f[len(path) - 1:] for f in sorted(glob.glob(path))
4932 if os.path.isdir(f) and not self.isprivate(f)]
4933 self.filenames = [f[len(path) - 1:] for f in sorted(glob.glob(path))
4934 if not os.path.isdir(f) and not self.isprivate(f)]
4935 if 'README' in self.filenames:
4936 readme = open(os.path.join(filename,'README')).read()
4937 self.paragraph = MARKMIN(readme)
4938 else:
4939 self.paragraph = None
4940 if extensions:
4941 self.filenames = [f for f in self.filenames
4942 if os.path.splitext(f)[-1] in extensions]
4943
4945 path = []
4946 span = SPAN()
4947 span.append(A(basename, _href=URL()))
4948 for arg in self.args:
4949 span.append('/')
4950 path.append(arg)
4951 span.append(A(arg, _href=URL(args='/'.join(path))))
4952 return span
4953
4955 if self.folders:
4956 return SPAN(H3('Folders'), TABLE(
4957 *[TR(TD(A(folder, _href=URL(args=self.args + [folder]))))
4958 for folder in self.folders],
4959 **dict(_class="table")))
4960 return ''
4961
4962 @staticmethod
4965
4966 @staticmethod
4968 return os.path.splitext(f)[-1].lower() in (
4969 '.png', '.jpg', '.jpeg', '.gif', '.tiff')
4970
4972 if self.filenames:
4973 return SPAN(H3('Files'),
4974 TABLE(*[TR(TD(A(f, _href=URL(args=self.args + [f]))),
4975 TD(IMG(_src=URL(args=self.args + [f]),
4976 _style='max-width:%spx' % width)
4977 if width and self.isimage(f) else ''))
4978 for f in self.filenames],
4979 **dict(_class="table")))
4980 return ''
4981
4988
4989
4990 -class Wiki(object):
4991 everybody = 'everybody'
4992 rows_page = 25
4994 return MARKMIN(body, extra=self.settings.extra,
4995 url=True, environment=self.env,
4996 autolinks=lambda link: expand_one(link, {})).xml()
4997
5003
5006
5017
5018 @staticmethod
5020 """
5021 In wiki docs allows @{component:controller/function/args}
5022 which renders as a LOAD(..., ajax=True)
5023 """
5024 items = text.split('/')
5025 controller, function, args = items[0], items[1], items[2:]
5026 return LOAD(controller, function, args=args, ajax=True).xml()
5027
5036
5037 - def __init__(self, auth, env=None, render='markmin',
5038 manage_permissions=False, force_prefix='',
5039 restrict_search=False, extra=None,
5040 menu_groups=None, templates=None, migrate=True,
5041 controller=None, function=None):
5042
5043 settings = self.settings = auth.settings.wiki
5044
5045
5046 settings.render = render
5047 perms = settings.manage_permissions = manage_permissions
5048
5049 settings.force_prefix = force_prefix
5050 settings.restrict_search = restrict_search
5051 settings.extra = extra or {}
5052 settings.menu_groups = menu_groups
5053 settings.templates = templates
5054 settings.controller = controller
5055 settings.function = function
5056
5057 db = auth.db
5058 self.env = env or {}
5059 self.env['component'] = Wiki.component
5060 self.auth = auth
5061 self.wiki_menu_items = None
5062
5063 if self.auth.user:
5064 self.settings.force_prefix = force_prefix % self.auth.user
5065 else:
5066 self.settings.force_prefix = force_prefix
5067
5068 self.host = current.request.env.http_host
5069
5070 table_definitions = [
5071 ('wiki_page', {
5072 'args':[
5073 Field('slug',
5074 requires=[IS_SLUG(),
5075 IS_NOT_IN_DB(db, 'wiki_page.slug')],
5076 writable=False),
5077 Field('title', unique=True),
5078 Field('body', 'text', notnull=True),
5079 Field('tags', 'list:string'),
5080 Field('can_read', 'list:string',
5081 writable=perms,
5082 readable=perms,
5083 default=[Wiki.everybody]),
5084 Field('can_edit', 'list:string',
5085 writable=perms, readable=perms,
5086 default=[Wiki.everybody]),
5087 Field('changelog'),
5088 Field('html', 'text',
5089 compute=self.get_render(),
5090 readable=False, writable=False),
5091 auth.signature],
5092 'vars':{'format':'%(title)s', 'migrate':migrate}}),
5093 ('wiki_tag', {
5094 'args':[
5095 Field('name'),
5096 Field('wiki_page', 'reference wiki_page'),
5097 auth.signature],
5098 'vars':{'format':'%(title)s', 'migrate':migrate}}),
5099 ('wiki_media', {
5100 'args':[
5101 Field('wiki_page', 'reference wiki_page'),
5102 Field('title', required=True),
5103 Field('filename', 'upload', required=True),
5104 auth.signature],
5105 'vars':{'format':'%(title)s', 'migrate':migrate}}),
5106 ]
5107
5108
5109 for key, value in table_definitions:
5110 args = []
5111 if not key in db.tables():
5112
5113 extra_fields = auth.settings.extra_fields
5114 if extra_fields:
5115 if key in extra_fields:
5116 if extra_fields[key]:
5117 for field in extra_fields[key]:
5118 args.append(field)
5119 args += value['args']
5120 db.define_table(key, *args, **value['vars'])
5121
5122 if self.settings.templates is None and not \
5123 self.settings.manage_permissions:
5124 self.settings.templates = db.wiki_page.tags.contains('template')&\
5125 db.wiki_page.can_read.contains('everybody')
5126
5127 def update_tags_insert(page, id, db=db):
5128 for tag in page.tags or []:
5129 tag = tag.strip().lower()
5130 if tag:
5131 db.wiki_tag.insert(name=tag, wiki_page=id)
5132
5133 def update_tags_update(dbset, page, db=db):
5134 page = dbset.select().first()
5135 db(db.wiki_tag.wiki_page == page.id).delete()
5136 for tag in page.tags or []:
5137 tag = tag.strip().lower()
5138 if tag:
5139 db.wiki_tag.insert(name=tag, wiki_page=page.id)
5140 db.wiki_page._after_insert.append(update_tags_insert)
5141 db.wiki_page._after_update.append(update_tags_update)
5142
5143 if (auth.user and
5144 check_credentials(current.request, gae_login=False) and
5145 not 'wiki_editor' in auth.user_groups.values()):
5146 group = db.auth_group(role='wiki_editor')
5147 gid = group.id if group else db.auth_group.insert(
5148 role='wiki_editor')
5149 auth.add_membership(gid)
5150
5151 settings.lock_keys = True
5152
5153
5154
5157
5159 if 'everybody' in page.can_read or not \
5160 self.settings.manage_permissions:
5161 return True
5162 elif self.auth.user:
5163 groups = self.auth.user_groups.values()
5164 if ('wiki_editor' in groups or
5165 set(groups).intersection(set(page.can_read + page.can_edit)) or
5166 page.created_by == self.auth.user.id):
5167 return True
5168 return False
5169
5179
5185
5188
5190 if self.settings.menu_groups is None:
5191 return True
5192 if self.auth.user:
5193 groups = self.auth.user_groups.values()
5194 if any(t in self.settings.menu_groups for t in groups):
5195 return True
5196 return False
5197
5198
5199
5207
5243
5245 if not self.can_read(page):
5246 mm = (page.body or '').replace('\r', '')
5247 ps = [p for p in mm.split('\n\n')
5248 if not p.startswith('#') and p.strip()]
5249 if ps:
5250 return ps[0]
5251 return ''
5252
5254 return (body or '').replace('://HOSTNAME', '://%s' % self.host)
5255
5256 - def read(self, slug):
5257 if slug in '_cloud':
5258 return self.cloud()
5259 elif slug in '_search':
5260 return self.search()
5261 page = self.auth.db.wiki_page(slug=slug)
5262 if not page:
5263 redirect(URL(args=('_create', slug)))
5264 if not self.can_read(page):
5265 return self.not_authorized(page)
5266 if current.request.extension == 'html':
5267 if not page:
5268 url = URL(args=('_edit', slug))
5269 return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn"))
5270 else:
5271 return dict(title=page.title,
5272 slug=page.slug,
5273 page=page,
5274 content=XML(self.fix_hostname(page.html)),
5275 tags=page.tags,
5276 created_on=page.created_on,
5277 modified_on=page.modified_on)
5278 elif current.request.extension == 'load':
5279 return self.fix_hostname(page.html) if page else ''
5280 else:
5281 if not page:
5282 raise HTTP(404)
5283 else:
5284 return dict(title=page.title,
5285 slug=page.slug,
5286 page=page,
5287 content=page.body,
5288 tags=page.tags,
5289 created_on=page.created_on,
5290 modified_on=page.modified_on)
5291
5293 if not self.auth.user:
5294 if not act:
5295 return False
5296 redirect(self.auth.settings.login_url)
5297 elif not self.auth.has_membership(role):
5298 if not act:
5299 return False
5300 raise HTTP(401, "Not Authorized")
5301 return True
5302
5303 - def edit(self,slug,from_template=0):
5304 auth = self.auth
5305 db = auth.db
5306 page = db.wiki_page(slug=slug)
5307 if not self.can_edit(page):
5308 return self.not_authorized(page)
5309 title_guess = ' '.join(c.capitalize() for c in slug.split('-'))
5310 if not page:
5311 if not (self.can_manage() or
5312 slug.startswith(self.settings.force_prefix)):
5313 current.session.flash = 'slug must have "%s" prefix' \
5314 % self.settings.force_prefix
5315 redirect(URL(args=('_create')))
5316 db.wiki_page.can_read.default = [Wiki.everybody]
5317 db.wiki_page.can_edit.default = [auth.user_group_role()]
5318 db.wiki_page.title.default = title_guess
5319 db.wiki_page.slug.default = slug
5320 if slug == 'wiki-menu':
5321 db.wiki_page.body.default = \
5322 '- Menu Item > @////index\n- - Submenu > http://web2py.com'
5323 else:
5324 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
5325 vars = current.request.post_vars
5326 if vars.body:
5327 vars.body = vars.body.replace('://%s' % self.host, '://HOSTNAME')
5328 form = SQLFORM(db.wiki_page, page, deletable=True,
5329 formstyle='table2cols', showid=False).process()
5330 if form.deleted:
5331 current.session.flash = 'page deleted'
5332 redirect(URL())
5333 elif form.accepted:
5334 current.session.flash = 'page created'
5335 redirect(URL(args=slug))
5336 script = """
5337 jQuery(function() {
5338 if (!jQuery('#wiki_page_body').length) return;
5339 var pagecontent = jQuery('#wiki_page_body');
5340 pagecontent.css('font-family',
5341 'Monaco,Menlo,Consolas,"Courier New",monospace');
5342 var prevbutton = jQuery('<button class="btn nopreview">Preview</button>');
5343 var preview = jQuery('<div id="preview"></div>').hide();
5344 var previewmedia = jQuery('<div id="previewmedia"></div>');
5345 var form = pagecontent.closest('form');
5346 preview.insertBefore(form);
5347 prevbutton.insertBefore(form);
5348 if(%(link_media)s) {
5349 var mediabutton = jQuery('<button class="btn nopreview">Media</button>');
5350 mediabutton.insertBefore(form);
5351 previewmedia.insertBefore(form);
5352 mediabutton.toggle(function() {
5353 web2py_component('%(urlmedia)s', 'previewmedia');
5354 }, function() {
5355 previewmedia.empty();
5356 });
5357 }
5358 prevbutton.click(function(e) {
5359 e.preventDefault();
5360 if (prevbutton.hasClass('nopreview')) {
5361 prevbutton.addClass('preview').removeClass(
5362 'nopreview').html('Edit Source');
5363 web2py_ajax_page('post', '%(url)s', {body : jQuery('#wiki_page_body').val()}, 'preview');
5364 form.fadeOut('fast', function() {preview.fadeIn()});
5365 } else {
5366 prevbutton.addClass(
5367 'nopreview').removeClass('preview').html('Preview');
5368 preview.fadeOut('fast', function() {form.fadeIn()});
5369 }
5370 })
5371 })
5372 """ % dict(url=URL(args=('_preview', slug)),link_media=('true' if page else 'false'),
5373 urlmedia=URL(extension='load',
5374 args=('_editmedia',slug),
5375 vars=dict(embedded=1)))
5376 return dict(content=TAG[''](form, SCRIPT(script)))
5377
5412
5414 if not self.can_edit():
5415 return self.not_authorized()
5416 db = self.auth.db
5417 slugs=db(db.wiki_page.id>0).select(db.wiki_page.id,db.wiki_page.slug)
5418 options=[OPTION(row.slug,_value=row.id) for row in slugs]
5419 options.insert(0, OPTION('',_value=''))
5420 fields = [Field("slug", default=current.request.args(1) or
5421 self.settings.force_prefix,
5422 requires=(IS_SLUG(), IS_NOT_IN_DB(db,db.wiki_page.slug))),]
5423 if self.settings.templates:
5424 fields.append(
5425 Field("from_template", "reference wiki_page",
5426 requires=IS_EMPTY_OR(
5427 IS_IN_DB(db(self.settings.templates),
5428 db.wiki_page._id,
5429 '%(slug)s')),
5430 comment=current.T(
5431 "Choose Template or empty for new Page")))
5432 form = SQLFORM.factory(*fields, **dict(_class="well"))
5433 form.element("[type=submit]").attributes["_value"] = \
5434 current.T("Create Page from Slug")
5435
5436 if form.process().accepted:
5437 form.vars.from_template = 0 if not form.vars.from_template \
5438 else form.vars.from_template
5439 redirect(URL(args=('_edit', form.vars.slug,form.vars.from_template or 0)))
5440 return dict(content=form)
5441
5443 if not self.can_manage():
5444 return self.not_authorized()
5445 self.auth.db.wiki_page.slug.represent = lambda slug, row: SPAN(
5446 '@////%s' % slug)
5447 self.auth.db.wiki_page.title.represent = lambda title, row: \
5448 A(title, _href=URL(args=row.slug))
5449 wiki_table = self.auth.db.wiki_page
5450 content = SQLFORM.grid(
5451 wiki_table,
5452 fields = [wiki_table.slug,
5453 wiki_table.title, wiki_table.tags,
5454 wiki_table.can_read, wiki_table.can_edit],
5455 links=[
5456 lambda row:
5457 A('edit', _href=URL(args=('_edit', row.slug)),_class='btn'),
5458 lambda row:
5459 A('media', _href=URL(args=('_editmedia', row.slug)),_class='btn')],
5460 details=False, editable=False, deletable=False, create=False,
5461 orderby=self.auth.db.wiki_page.title,
5462 args=['_pages'],
5463 user_signature=False)
5464
5465 return dict(content=content)
5466
5479
5481 db = self.auth.db
5482 request = current.request
5483 menu_page = db.wiki_page(slug='wiki-menu')
5484 menu = []
5485 if menu_page:
5486 tree = {'': menu}
5487 regex = re.compile('[\r\n\t]*(?P<base>(\s*\-\s*)+)(?P<title>\w.*?)\s+\>\s+(?P<link>\S+)')
5488 for match in regex.finditer(self.fix_hostname(menu_page.body)):
5489 base = match.group('base').replace(' ', '')
5490 title = match.group('title')
5491 link = match.group('link')
5492 title_page = None
5493 if link.startswith('@'):
5494 items = link[2:].split('/')
5495 if len(items) > 3:
5496 title_page = items[3]
5497 link = URL(a=items[0] or None, c=items[1] or controller,
5498 f=items[2] or function, args=items[3:])
5499 parent = tree.get(base[1:], tree[''])
5500 subtree = []
5501 tree[base] = subtree
5502 parent.append((current.T(title),
5503 request.args(0) == title_page,
5504 link, subtree))
5505 if self.can_see_menu():
5506 submenu = []
5507 menu.append((current.T('[Wiki]'), None, None, submenu))
5508 if URL() == URL(controller, function):
5509 if not str(request.args(0)).startswith('_'):
5510 slug = request.args(0) or 'index'
5511 mode = 1
5512 elif request.args(0) == '_edit':
5513 slug = request.args(1) or 'index'
5514 mode = 2
5515 elif request.args(0) == '_editmedia':
5516 slug = request.args(1) or 'index'
5517 mode = 3
5518 else:
5519 mode = 0
5520 if mode in (2, 3):
5521 submenu.append((current.T('View Page'), None,
5522 URL(controller, function, args=slug)))
5523 if mode in (1, 3):
5524 submenu.append((current.T('Edit Page'), None,
5525 URL(controller, function, args=('_edit', slug))))
5526 if mode in (1, 2):
5527 submenu.append((current.T('Edit Page Media'), None,
5528 URL(controller, function, args=('_editmedia', slug))))
5529
5530 submenu.append((current.T('Create New Page'), None,
5531 URL(controller, function, args=('_create'))))
5532
5533 if self.can_manage():
5534 submenu.append((current.T('Manage Pages'), None,
5535 URL(controller, function, args=('_pages'))))
5536 submenu.append((current.T('Edit Menu'), None,
5537 URL(controller, function, args=('_edit', 'wiki-menu'))))
5538
5539 submenu.append((current.T('Search Pages'), None,
5540 URL(controller, function, args=('_search'))))
5541 return menu
5542
5543 - def search(self, tags=None, query=None, cloud=True, preview=True,
5544 limitby=(0, 100), orderby=None):
5545 if not self.can_search():
5546 return self.not_authorized()
5547 request = current.request
5548 content = CAT()
5549 if tags is None and query is None:
5550 form = FORM(INPUT(_name='q', requires=IS_NOT_EMPTY(),
5551 value=request.vars.q),
5552 INPUT(_type="submit", _value=current.T('Search')),
5553 _method='GET')
5554 content.append(DIV(form, _class='w2p_wiki_form'))
5555 if request.vars.q:
5556 tags = [v.strip() for v in request.vars.q.split(',')]
5557 tags = [v.lower() for v in tags if v]
5558 if tags or not query is None:
5559 db = self.auth.db
5560 count = db.wiki_tag.wiki_page.count()
5561 fields = [db.wiki_page.id, db.wiki_page.slug,
5562 db.wiki_page.title, db.wiki_page.tags,
5563 db.wiki_page.can_read]
5564 if preview:
5565 fields.append(db.wiki_page.body)
5566 if query is None:
5567 query = (db.wiki_page.id == db.wiki_tag.wiki_page) &\
5568 (db.wiki_tag.name.belongs(tags))
5569 query = query | db.wiki_page.title.contains(request.vars.q)
5570 if self.settings.restrict_search and not self.manage():
5571 query = query & (db.wiki_page.created_by == self.auth.user_id)
5572 pages = db(query).select(count,
5573 *fields, **dict(orderby=orderby or ~count,
5574 groupby=reduce(lambda a, b: a | b, fields),
5575 distinct=True,
5576 limitby=limitby))
5577 if request.extension in ('html', 'load'):
5578 if not pages:
5579 content.append(DIV(current.T("No results"),
5580 _class='w2p_wiki_form'))
5581
5582 def link(t):
5583 return A(t, _href=URL(args='_search', vars=dict(q=t)))
5584 items = [DIV(H3(A(p.wiki_page.title, _href=URL(
5585 args=p.wiki_page.slug))),
5586 MARKMIN(self.first_paragraph(p.wiki_page))
5587 if preview else '',
5588 DIV(_class='w2p_wiki_tags',
5589 *[link(t.strip()) for t in
5590 p.wiki_page.tags or [] if t.strip()]),
5591 _class='w2p_wiki_search_item')
5592 for p in pages]
5593 content.append(DIV(_class='w2p_wiki_pages', *items))
5594 else:
5595 cloud = False
5596 content = [p.wiki_page.as_dict() for p in pages]
5597 elif cloud:
5598 content.append(self.cloud()['content'])
5599 if request.extension == 'load':
5600 return content
5601 return dict(content=content)
5602
5604 db = self.auth.db
5605 count = db.wiki_tag.wiki_page.count(distinct=True)
5606 ids = db(db.wiki_tag).select(
5607 db.wiki_tag.name, count,
5608 distinct=True,
5609 groupby=db.wiki_tag.name,
5610 orderby=~count, limitby=(0, 20))
5611 if ids:
5612 a, b = ids[0](count), ids[-1](count)
5613
5614 def style(c):
5615 STYLE = 'padding:0 0.2em;line-height:%.2fem;font-size:%.2fem'
5616 size = (1.5 * (c - b) / max(a - b, 1) + 1.3)
5617 return STYLE % (1.3, size)
5618 items = []
5619 for item in ids:
5620 items.append(A(item.wiki_tag.name,
5621 _style=style(item(count)),
5622 _href=URL(args='_search',
5623 vars=dict(q=item.wiki_tag.name))))
5624 items.append(' ')
5625 return dict(content=DIV(_class='w2p_cloud', *items))
5626
5630
5631
5632 if __name__ == '__main__':
5633 import doctest
5634 doctest.testmod()
5635