Package web2py :: Package gluon :: Module tools
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.tools

   1  #!/bin/python 
   2  # -*- coding: utf-8 -*- 
   3   
   4  """ 
   5  This file is part of the web2py Web Framework 
   6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
   7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
   8  """ 
   9   
  10  import base64 
  11  import cPickle 
  12  import datetime 
  13  import thread 
  14  import logging 
  15  import sys 
  16  import os 
  17  import re 
  18  import time 
  19  import copy 
  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 
  26   
  27  from contenttype import contenttype 
  28  from storage import Storage, StorageList, Settings, Messages 
  29  from utils import web2py_uuid 
  30  from gluon import * 
  31   
  32  import serializers 
  33  import contrib.simplejson as simplejson 
  34   
  35   
  36  __all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'PluginManager', 'fetch', 'geocode', 'prettydate'] 
  37   
  38  logger = logging.getLogger("web2py") 
  39   
  40  DEFAULT = lambda: None 
  41   
42 -def callback(actions,form,tablename=None):
43 if actions: 44 if tablename and isinstance(actions,dict): 45 actions = actions.get(tablename, []) 46 if not isinstance(actions,(list, tuple)): 47 actions = [actions] 48 [action(form) for action in actions]
49
50 -def validators(*a):
51 b = [] 52 for item in a: 53 if isinstance(item, (list, tuple)): 54 b = b + list(item) 55 else: 56 b.append(item) 57 return b
58
59 -def call_or_redirect(f,*args):
60 if callable(f): 61 redirect(f(*args)) 62 else: 63 redirect(f)
64
65 -class Mail(object):
66 """ 67 Class for configuring and sending emails with alternative text / html 68 body, multiple attachments and encryption support 69 70 Works with SMTP and Google App Engine. 71 """ 72
73 - class Attachment(MIMEBase.MIMEBase):
74 """ 75 Email attachment 76 77 Arguments:: 78 79 payload: path to file or file-like object with read() method 80 filename: name of the attachment stored in message; if set to 81 None, it will be fetched from payload path; file-like 82 object payload must have explicit filename specified 83 content_id: id of the attachment; automatically contained within 84 < and > 85 content_type: content type of the attachment; if set to None, 86 it will be fetched from filename using gluon.contenttype 87 module 88 encoding: encoding of all strings passed to this function (except 89 attachment body) 90 91 Content ID is used to identify attachments within the html body; 92 in example, attached image with content ID 'photo' may be used in 93 html message as a source of img tag <img src="cid:photo" />. 94 95 Examples:: 96 97 #Create attachment from text file: 98 attachment = Mail.Attachment('/path/to/file.txt') 99 100 Content-Type: text/plain 101 MIME-Version: 1.0 102 Content-Disposition: attachment; filename="file.txt" 103 Content-Transfer-Encoding: base64 104 105 SOMEBASE64CONTENT= 106 107 #Create attachment from image file with custom filename and cid: 108 attachment = Mail.Attachment('/path/to/file.png', 109 filename='photo.png', 110 content_id='photo') 111 112 Content-Type: image/png 113 MIME-Version: 1.0 114 Content-Disposition: attachment; filename="photo.png" 115 Content-Id: <photo> 116 Content-Transfer-Encoding: base64 117 118 SOMEOTHERBASE64CONTENT= 119 """ 120
121 - def __init__( 122 self, 123 payload, 124 filename=None, 125 content_id=None, 126 content_type=None, 127 encoding='utf-8'):
128 if isinstance(payload, str): 129 if filename == None: 130 filename = os.path.basename(payload) 131 handle = open(payload, 'rb') 132 payload = handle.read() 133 handle.close() 134 else: 135 if filename == None: 136 raise Exception('Missing attachment name') 137 payload = payload.read() 138 filename = filename.encode(encoding) 139 if content_type == None: 140 content_type = contenttype(filename) 141 self.my_filename = filename 142 self.my_payload = payload 143 MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1)) 144 self.set_payload(payload) 145 self['Content-Disposition'] = 'attachment; filename="%s"' % filename 146 if content_id != None: 147 self['Content-Id'] = '<%s>' % content_id.encode(encoding) 148 Encoders.encode_base64(self)
149
150 - def __init__(self, server=None, sender=None, login=None, tls=True):
151 """ 152 Main Mail object 153 154 Arguments:: 155 156 server: SMTP server address in address:port notation 157 sender: sender email address 158 login: sender login name and password in login:password notation 159 or None if no authentication is required 160 tls: enables/disables encryption (True by default) 161 162 In Google App Engine use:: 163 164 server='gae' 165 166 For sake of backward compatibility all fields are optional and default 167 to None, however, to be able to send emails at least server and sender 168 must be specified. They are available under following fields: 169 170 mail.settings.server 171 mail.settings.sender 172 mail.settings.login 173 174 When server is 'logging', email is logged but not sent (debug mode) 175 176 Optionally you can use PGP encryption or X509: 177 178 mail.settings.cipher_type = None 179 mail.settings.sign = True 180 mail.settings.sign_passphrase = None 181 mail.settings.encrypt = True 182 mail.settings.x509_sign_keyfile = None 183 mail.settings.x509_sign_certfile = None 184 mail.settings.x509_crypt_certfiles = None 185 186 cipher_type : None 187 gpg - need a python-pyme package and gpgme lib 188 x509 - smime 189 sign : sign the message (True or False) 190 sign_passphrase : passphrase for key signing 191 encrypt : encrypt the message 192 ... x509 only ... 193 x509_sign_keyfile : the signers private key filename (PEM format) 194 x509_sign_certfile: the signers certificate filename (PEM format) 195 x509_crypt_certfiles: the certificates file to encrypt the messages 196 with can be a file name or a list of 197 file names (PEM format) 198 199 Examples:: 200 201 #Create Mail object with authentication data for remote server: 202 mail = Mail('example.com:25', 'me@example.com', 'me:password') 203 """ 204 205 settings = self.settings = Settings() 206 settings.server = server 207 settings.sender = sender 208 settings.login = login 209 settings.tls = tls 210 settings.cipher_type = None 211 settings.sign = True 212 settings.sign_passphrase = None 213 settings.encrypt = True 214 settings.x509_sign_keyfile = None 215 settings.x509_sign_certfile = None 216 settings.x509_crypt_certfiles = None 217 settings.debug = False 218 settings.lock_keys = True 219 self.result = {} 220 self.error = None
221
222 - def send( 223 self, 224 to, 225 subject='None', 226 message='None', 227 attachments=None, 228 cc=None, 229 bcc=None, 230 reply_to=None, 231 encoding='utf-8', 232 ):
233 """ 234 Sends an email using data specified in constructor 235 236 Arguments:: 237 238 to: list or tuple of receiver addresses; will also accept single 239 object 240 subject: subject of the email 241 message: email body text; depends on type of passed object: 242 if 2-list or 2-tuple is passed: first element will be 243 source of plain text while second of html text; 244 otherwise: object will be the only source of plain text 245 and html source will be set to None; 246 If text or html source is: 247 None: content part will be ignored, 248 string: content part will be set to it, 249 file-like object: content part will be fetched from 250 it using it's read() method 251 attachments: list or tuple of Mail.Attachment objects; will also 252 accept single object 253 cc: list or tuple of carbon copy receiver addresses; will also 254 accept single object 255 bcc: list or tuple of blind carbon copy receiver addresses; will 256 also accept single object 257 reply_to: address to which reply should be composed 258 encoding: encoding of all strings passed to this method (including 259 message bodies) 260 261 Examples:: 262 263 #Send plain text message to single address: 264 mail.send('you@example.com', 265 'Message subject', 266 'Plain text body of the message') 267 268 #Send html message to single address: 269 mail.send('you@example.com', 270 'Message subject', 271 '<html>Plain text body of the message</html>') 272 273 #Send text and html message to three addresses (two in cc): 274 mail.send('you@example.com', 275 'Message subject', 276 ('Plain text body', '<html>html body</html>'), 277 cc=['other1@example.com', 'other2@example.com']) 278 279 #Send html only message with image attachment available from 280 the message by 'photo' content id: 281 mail.send('you@example.com', 282 'Message subject', 283 (None, '<html><img src="cid:photo" /></html>'), 284 Mail.Attachment('/path/to/photo.jpg' 285 content_id='photo')) 286 287 #Send email with two attachments and no body text 288 mail.send('you@example.com, 289 'Message subject', 290 None, 291 [Mail.Attachment('/path/to/fist.file'), 292 Mail.Attachment('/path/to/second.file')]) 293 294 Returns True on success, False on failure. 295 296 Before return, method updates two object's fields: 297 self.result: return value of smtplib.SMTP.sendmail() or GAE's 298 mail.send_mail() method 299 self.error: Exception message or None if above was successful 300 """ 301 302 def encode_header(key): 303 if [c for c in key if 32>ord(c) or ord(c)>127]: 304 return Header.Header(key.encode('utf-8'),'utf-8') 305 else: 306 return key
307 308 if not isinstance(self.settings.server, str): 309 raise Exception('Server address not specified') 310 if not isinstance(self.settings.sender, str): 311 raise Exception('Sender address not specified') 312 payload_in = MIMEMultipart.MIMEMultipart('mixed') 313 if to: 314 if not isinstance(to, (list,tuple)): 315 to = [to] 316 else: 317 raise Exception('Target receiver address not specified') 318 if cc: 319 if not isinstance(cc, (list, tuple)): 320 cc = [cc] 321 if bcc: 322 if not isinstance(bcc, (list, tuple)): 323 bcc = [bcc] 324 if message == None: 325 text = html = None 326 elif isinstance(message, (list, tuple)): 327 text, html = message 328 elif message.strip().startswith('<html') and message.strip().endswith('</html>'): 329 text = self.settings.server=='gae' and message or None 330 html = message 331 else: 332 text = message 333 html = None 334 if text != None or html != None: 335 attachment = MIMEMultipart.MIMEMultipart('alternative') 336 if text != None: 337 if isinstance(text, basestring): 338 text = text.decode(encoding).encode('utf-8') 339 else: 340 text = text.read().decode(encoding).encode('utf-8') 341 attachment.attach(MIMEText.MIMEText(text,_charset='utf-8')) 342 if html != None: 343 if isinstance(html, basestring): 344 html = html.decode(encoding).encode('utf-8') 345 else: 346 html = html.read().decode(encoding).encode('utf-8') 347 attachment.attach(MIMEText.MIMEText(html, 'html',_charset='utf-8')) 348 payload_in.attach(attachment) 349 if attachments == None: 350 pass 351 elif isinstance(attachments, (list, tuple)): 352 for attachment in attachments: 353 payload_in.attach(attachment) 354 else: 355 payload_in.attach(attachments) 356 357 358 ####################################################### 359 # CIPHER # 360 ####################################################### 361 cipher_type = self.settings.cipher_type 362 sign = self.settings.sign 363 sign_passphrase = self.settings.sign_passphrase 364 encrypt = self.settings.encrypt 365 ####################################################### 366 # GPGME # 367 ####################################################### 368 if cipher_type == 'gpg': 369 if not sign and not encrypt: 370 self.error="No sign and no encrypt is set but cipher type to gpg" 371 return False 372 373 # need a python-pyme package and gpgme lib 374 from pyme import core, errors 375 from pyme.constants.sig import mode 376 ############################################ 377 # sign # 378 ############################################ 379 if sign: 380 import string 381 core.check_version(None) 382 pin=string.replace(payload_in.as_string(),'\n','\r\n') 383 plain = core.Data(pin) 384 sig = core.Data() 385 c = core.Context() 386 c.set_armor(1) 387 c.signers_clear() 388 # search for signing key for From: 389 for sigkey in c.op_keylist_all(self.settings.sender, 1): 390 if sigkey.can_sign: 391 c.signers_add(sigkey) 392 if not c.signers_enum(0): 393 self.error='No key for signing [%s]' % self.settings.sender 394 return False 395 c.set_passphrase_cb(lambda x,y,z: sign_passphrase) 396 try: 397 # make a signature 398 c.op_sign(plain,sig,mode.DETACH) 399 sig.seek(0,0) 400 # make it part of the email 401 payload=MIMEMultipart.MIMEMultipart('signed', 402 boundary=None, 403 _subparts=None, 404 **dict(micalg="pgp-sha1", 405 protocol="application/pgp-signature")) 406 # insert the origin payload 407 payload.attach(payload_in) 408 # insert the detached signature 409 p=MIMEBase.MIMEBase("application",'pgp-signature') 410 p.set_payload(sig.read()) 411 payload.attach(p) 412 # it's just a trick to handle the no encryption case 413 payload_in=payload 414 except errors.GPGMEError, ex: 415 self.error="GPG error: %s" % ex.getstring() 416 return False 417 ############################################ 418 # encrypt # 419 ############################################ 420 if encrypt: 421 core.check_version(None) 422 plain = core.Data(payload_in.as_string()) 423 cipher = core.Data() 424 c = core.Context() 425 c.set_armor(1) 426 # collect the public keys for encryption 427 recipients=[] 428 rec=to[:] 429 if cc: 430 rec.extend(cc) 431 if bcc: 432 rec.extend(bcc) 433 for addr in rec: 434 c.op_keylist_start(addr,0) 435 r = c.op_keylist_next() 436 if r == None: 437 self.error='No key for [%s]' % addr 438 return False 439 recipients.append(r) 440 try: 441 # make the encryption 442 c.op_encrypt(recipients, 1, plain, cipher) 443 cipher.seek(0,0) 444 # make it a part of the email 445 payload=MIMEMultipart.MIMEMultipart('encrypted', 446 boundary=None, 447 _subparts=None, 448 **dict(protocol="application/pgp-encrypted")) 449 p=MIMEBase.MIMEBase("application",'pgp-encrypted') 450 p.set_payload("Version: 1\r\n") 451 payload.attach(p) 452 p=MIMEBase.MIMEBase("application",'octet-stream') 453 p.set_payload(cipher.read()) 454 payload.attach(p) 455 except errors.GPGMEError, ex: 456 self.error="GPG error: %s" % ex.getstring() 457 return False 458 ####################################################### 459 # X.509 # 460 ####################################################### 461 elif cipher_type == 'x509': 462 if not sign and not encrypt: 463 self.error="No sign and no encrypt is set but cipher type to x509" 464 return False 465 x509_sign_keyfile=self.settings.x509_sign_keyfile 466 if self.settings.x509_sign_certfile: 467 x509_sign_certfile=self.settings.x509_sign_certfile 468 else: 469 # if there is no sign certfile we'll assume the 470 # cert is in keyfile 471 x509_sign_certfile=self.settings.x509_sign_keyfile 472 # crypt certfiles could be a string or a list 473 x509_crypt_certfiles=self.settings.x509_crypt_certfiles 474 475 476 # need m2crypto 477 from M2Crypto import BIO, SMIME, X509 478 msg_bio = BIO.MemoryBuffer(payload_in.as_string()) 479 s = SMIME.SMIME() 480 481 # SIGN 482 if sign: 483 #key for signing 484 try: 485 s.load_key(x509_sign_keyfile, x509_sign_certfile, callback=lambda x: sign_passphrase) 486 if encrypt: 487 p7 = s.sign(msg_bio) 488 else: 489 p7 = s.sign(msg_bio,flags=SMIME.PKCS7_DETACHED) 490 msg_bio = BIO.MemoryBuffer(payload_in.as_string()) # Recreate coz sign() has consumed it. 491 except Exception,e: 492 self.error="Something went wrong on signing: <%s>" %str(e) 493 return False 494 495 # ENCRYPT 496 if encrypt: 497 try: 498 sk = X509.X509_Stack() 499 if not isinstance(x509_crypt_certfiles, (list, tuple)): 500 x509_crypt_certfiles = [x509_crypt_certfiles] 501 502 # make an encryption cert's stack 503 for x in x509_crypt_certfiles: 504 sk.push(X509.load_cert(x)) 505 s.set_x509_stack(sk) 506 507 s.set_cipher(SMIME.Cipher('des_ede3_cbc')) 508 tmp_bio = BIO.MemoryBuffer() 509 if sign: 510 s.write(tmp_bio, p7) 511 else: 512 tmp_bio.write(payload_in.as_string()) 513 p7 = s.encrypt(tmp_bio) 514 except Exception,e: 515 self.error="Something went wrong on encrypting: <%s>" %str(e) 516 return False 517 518 # Final stage in sign and encryption 519 out = BIO.MemoryBuffer() 520 if encrypt: 521 s.write(out, p7) 522 else: 523 if sign: 524 s.write(out, p7, msg_bio, SMIME.PKCS7_DETACHED) 525 else: 526 out.write('\r\n') 527 out.write(payload_in.as_string()) 528 out.close() 529 st=str(out.read()) 530 payload=message_from_string(st) 531 else: 532 # no cryptography process as usual 533 payload=payload_in 534 payload['From'] = encode_header(self.settings.sender.decode(encoding)) 535 origTo = to[:] 536 if to: 537 payload['To'] = encode_header(', '.join(to).decode(encoding)) 538 if reply_to: 539 payload['Reply-To'] = encode_header(reply_to.decode(encoding)) 540 if cc: 541 payload['Cc'] = encode_header(', '.join(cc).decode(encoding)) 542 to.extend(cc) 543 if bcc: 544 to.extend(bcc) 545 payload['Subject'] = encode_header(subject.decode(encoding)) 546 payload['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", 547 time.gmtime()) 548 result = {} 549 try: 550 if self.settings.server == 'logging': 551 logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\n\n%s\n%s\n' % \ 552 ('-'*40,self.settings.sender, 553 ', '.join(to),text or html,'-'*40)) 554 elif self.settings.server == 'gae': 555 xcc = dict() 556 if cc: 557 xcc['cc'] = cc 558 if bcc: 559 xcc['bcc'] = bcc 560 from google.appengine.api import mail 561 attachments = attachments and [(a.my_filename,a.my_payload) for a in attachments] 562 if attachments: 563 result = mail.send_mail(sender=self.settings.sender, to=origTo, 564 subject=subject, body=text, html=html, 565 attachments=attachments, **xcc) 566 elif html: 567 result = mail.send_mail(sender=self.settings.sender, to=origTo, 568 subject=subject, body=text, html=html, **xcc) 569 else: 570 result = mail.send_mail(sender=self.settings.sender, to=origTo, 571 subject=subject, body=text, **xcc) 572 else: 573 server = smtplib.SMTP(*self.settings.server.split(':')) 574 if self.settings.login != None: 575 if self.settings.tls: 576 server.ehlo() 577 server.starttls() 578 server.ehlo() 579 server.login(*self.settings.login.split(':',1)) 580 result = server.sendmail(self.settings.sender, to, payload.as_string()) 581 server.quit() 582 except Exception, e: 583 logger.warn('Mail.send failure:%s' % e) 584 self.result = result 585 self.error = e 586 return False 587 self.result = result 588 self.error = None 589 return True
590 591
592 -class Recaptcha(DIV):
593 594 API_SSL_SERVER = 'https://www.google.com/recaptcha/api' 595 API_SERVER = 'http://www.google.com/recaptcha/api' 596 VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify' 597
598 - def __init__( 599 self, 600 request, 601 public_key='', 602 private_key='', 603 use_ssl=False, 604 error=None, 605 error_message='invalid', 606 label = 'Verify:', 607 options = '' 608 ):
609 self.remote_addr = request.env.remote_addr 610 self.public_key = public_key 611 self.private_key = private_key 612 self.use_ssl = use_ssl 613 self.error = error 614 self.errors = Storage() 615 self.error_message = error_message 616 self.components = [] 617 self.attributes = {} 618 self.label = label 619 self.options = options 620 self.comment = ''
621
622 - def _validate(self):
623 624 # for local testing: 625 626 recaptcha_challenge_field = \ 627 self.request_vars.recaptcha_challenge_field 628 recaptcha_response_field = \ 629 self.request_vars.recaptcha_response_field 630 private_key = self.private_key 631 remoteip = self.remote_addr 632 if not (recaptcha_response_field and recaptcha_challenge_field 633 and len(recaptcha_response_field) 634 and len(recaptcha_challenge_field)): 635 self.errors['captcha'] = self.error_message 636 return False 637 params = urllib.urlencode({ 638 'privatekey': private_key, 639 'remoteip': remoteip, 640 'challenge': recaptcha_challenge_field, 641 'response': recaptcha_response_field, 642 }) 643 request = urllib2.Request( 644 url=self.VERIFY_SERVER, 645 data=params, 646 headers={'Content-type': 'application/x-www-form-urlencoded', 647 'User-agent': 'reCAPTCHA Python'}) 648 httpresp = urllib2.urlopen(request) 649 return_values = httpresp.read().splitlines() 650 httpresp.close() 651 return_code = return_values[0] 652 if return_code == 'true': 653 del self.request_vars.recaptcha_challenge_field 654 del self.request_vars.recaptcha_response_field 655 self.request_vars.captcha = '' 656 return True 657 self.errors['captcha'] = self.error_message 658 return False
659
660 - def xml(self):
661 public_key = self.public_key 662 use_ssl = self.use_ssl 663 error_param = '' 664 if self.error: 665 error_param = '&error=%s' % self.error 666 if use_ssl: 667 server = self.API_SSL_SERVER 668 else: 669 server = self.API_SERVER 670 captcha = DIV( 671 SCRIPT("var RecaptchaOptions = {%s};" % self.options), 672 SCRIPT(_type="text/javascript", 673 _src="%s/challenge?k=%s%s" % (server,public_key,error_param)), 674 TAG.noscript(IFRAME(_src="%s/noscript?k=%s%s" % (server,public_key,error_param), 675 _height="300",_width="500",_frameborder="0"), BR(), 676 INPUT(_type='hidden', _name='recaptcha_response_field', 677 _value='manual_challenge')), _id='recaptcha') 678 if not self.errors.captcha: 679 return XML(captcha).xml() 680 else: 681 captcha.append(DIV(self.errors['captcha'], _class='error')) 682 return XML(captcha).xml()
683 684
685 -def addrow(form,a,b,c,style,_id,position=-1):
686 if style == "divs": 687 form[0].insert(position, DIV(DIV(LABEL(a),_class='w2p_fl'), 688 DIV(b, _class='w2p_fw'), 689 DIV(c, _class='w2p_fc'), 690 _id = _id)) 691 elif style == "table2cols": 692 form[0].insert(position, TR(LABEL(a),'')) 693 form[0].insert(position+1, TR(b, _colspan=2, _id = _id)) 694 elif style == "ul": 695 form[0].insert(position, LI(DIV(LABEL(a),_class='w2p_fl'), 696 DIV(b, _class='w2p_fw'), 697 DIV(c, _class='w2p_fc'), 698 _id = _id)) 699 else: 700 form[0].insert(position, TR(LABEL(a),b,c,_id = _id))
701 702
703 -class Auth(object):
704 """ 705 Class for authentication, authorization, role based access control. 706 707 Includes: 708 709 - registration and profile 710 - login and logout 711 - username and password retrieval 712 - event logging 713 - role creation and assignment 714 - user defined group/role based permission 715 716 Authentication Example:: 717 718 from contrib.utils import * 719 mail=Mail() 720 mail.settings.server='smtp.gmail.com:587' 721 mail.settings.sender='you@somewhere.com' 722 mail.settings.login='username:password' 723 auth=Auth(globals(), db) 724 auth.settings.mailer=mail 725 # auth.settings....=... 726 auth.define_tables() 727 def authentication(): 728 return dict(form=auth()) 729 730 exposes: 731 732 - http://.../{application}/{controller}/authentication/login 733 - http://.../{application}/{controller}/authentication/logout 734 - http://.../{application}/{controller}/authentication/register 735 - http://.../{application}/{controller}/authentication/verify_email 736 - http://.../{application}/{controller}/authentication/retrieve_username 737 - http://.../{application}/{controller}/authentication/retrieve_password 738 - http://.../{application}/{controller}/authentication/reset_password 739 - http://.../{application}/{controller}/authentication/profile 740 - http://.../{application}/{controller}/authentication/change_password 741 742 On registration a group with role=new_user.id is created 743 and user is given membership of this group. 744 745 You can create a group with:: 746 747 group_id=auth.add_group('Manager', 'can access the manage action') 748 auth.add_permission(group_id, 'access to manage') 749 750 Here \"access to manage\" is just a user defined string. 751 You can give access to a user:: 752 753 auth.add_membership(group_id, user_id) 754 755 If user id is omitted, the logged in user is assumed 756 757 Then you can decorate any action:: 758 759 @auth.requires_permission('access to manage') 760 def manage(): 761 return dict() 762 763 You can restrict a permission to a specific table:: 764 765 auth.add_permission(group_id, 'edit', db.sometable) 766 @auth.requires_permission('edit', db.sometable) 767 768 Or to a specific record:: 769 770 auth.add_permission(group_id, 'edit', db.sometable, 45) 771 @auth.requires_permission('edit', db.sometable, 45) 772 773 If authorization is not granted calls:: 774 775 auth.settings.on_failed_authorization 776 777 Other options:: 778 779 auth.settings.mailer=None 780 auth.settings.expiration=3600 # seconds 781 782 ... 783 784 ### these are messages that can be customized 785 ... 786 """ 787 788
789 - def url(self, f=None, args=[], vars={}):
790 return URL(c=self.settings.controller,f=f,args=args,vars=vars)
791
792 - def __init__(self, environment=None, db=None, 793 controller='default', cas_provider = None):
794 """ 795 auth=Auth(globals(), db) 796 797 - environment is there for legacy but unused (awful) 798 - db has to be the database where to create tables for authentication 799 800 """ 801 ## next two lines for backward compatibility 802 if not db and environment and isinstance(environment,DAL): 803 db = environment 804 self.db = db 805 self.environment = current 806 request = current.request 807 session = current.session 808 auth = session.auth 809 if auth and auth.last_visit and auth.last_visit + \ 810 datetime.timedelta(days=0, seconds=auth.expiration) > request.now: 811 self.user = auth.user 812 # this is a trick to speed up sessions 813 if (request.now - auth.last_visit).seconds > (auth.expiration/10): 814 auth.last_visit = request.now 815 else: 816 self.user = None 817 session.auth = None 818 settings = self.settings = Settings() 819 820 # ## what happens after login? 821 822 # ## what happens after registration? 823 824 settings.hideerror = False 825 settings.cas_domains = [request.env.http_host] 826 settings.cas_provider = cas_provider 827 settings.extra_fields = {} 828 settings.actions_disabled = [] 829 settings.reset_password_requires_verification = False 830 settings.registration_requires_verification = False 831 settings.registration_requires_approval = False 832 settings.alternate_requires_registration = False 833 settings.create_user_groups = True 834 835 settings.controller = controller 836 settings.login_url = self.url('user', args='login') 837 settings.logged_url = self.url('user', args='profile') 838 settings.download_url = self.url('download') 839 settings.mailer = None 840 settings.login_captcha = None 841 settings.register_captcha = None 842 settings.retrieve_username_captcha = None 843 settings.retrieve_password_captcha = None 844 settings.captcha = None 845 settings.expiration = 3600 # one hour 846 settings.long_expiration = 3600*30*24 # one month 847 settings.remember_me_form = True 848 settings.allow_basic_login = False 849 settings.allow_basic_login_only = False 850 settings.on_failed_authorization = \ 851 self.url('user',args='not_authorized') 852 853 settings.on_failed_authentication = lambda x: redirect(x) 854 855 settings.formstyle = 'table3cols' 856 857 # ## table names to be used 858 859 settings.password_field = 'password' 860 settings.table_user_name = 'auth_user' 861 settings.table_group_name = 'auth_group' 862 settings.table_membership_name = 'auth_membership' 863 settings.table_permission_name = 'auth_permission' 864 settings.table_event_name = 'auth_event' 865 settings.table_cas_name = 'auth_cas' 866 867 # ## if none, they will be created 868 869 settings.table_user = None 870 settings.table_group = None 871 settings.table_membership = None 872 settings.table_permission = None 873 settings.table_event = None 874 settings.table_cas = None 875 876 # ## 877 878 settings.showid = False 879 880 # ## these should be functions or lambdas 881 882 settings.login_next = self.url('index') 883 settings.login_onvalidation = [] 884 settings.login_onaccept = [] 885 settings.login_methods = [self] 886 settings.login_form = self 887 settings.login_email_validate = True 888 settings.login_userfield = None 889 890 settings.logout_next = self.url('index') 891 settings.logout_onlogout = None 892 893 settings.register_next = self.url('index') 894 settings.register_onvalidation = [] 895 settings.register_onaccept = [] 896 settings.register_fields = None 897 898 settings.verify_email_next = self.url('user', args='login') 899 settings.verify_email_onaccept = [] 900 901 settings.profile_next = self.url('index') 902 settings.profile_onvalidation = [] 903 settings.profile_onaccept = [] 904 settings.profile_fields = None 905 settings.retrieve_username_next = self.url('index') 906 settings.retrieve_password_next = self.url('index') 907 settings.request_reset_password_next = self.url('user', args='login') 908 settings.reset_password_next = self.url('user', args='login') 909 910 settings.change_password_next = self.url('index') 911 settings.change_password_onvalidation = [] 912 settings.change_password_onaccept = [] 913 914 settings.retrieve_password_onvalidation = [] 915 settings.reset_password_onvalidation = [] 916 917 settings.hmac_key = None 918 settings.lock_keys = True 919 920 921 # ## these are messages that can be customized 922 messages = self.messages = Messages(current.T) 923 messages.submit_button = 'Submit' 924 messages.verify_password = 'Verify Password' 925 messages.delete_label = 'Check to delete:' 926 messages.function_disabled = 'Function disabled' 927 messages.access_denied = 'Insufficient privileges' 928 messages.registration_verifying = 'Registration needs verification' 929 messages.registration_pending = 'Registration is pending approval' 930 messages.login_disabled = 'Login disabled by administrator' 931 messages.logged_in = 'Logged in' 932 messages.email_sent = 'Email sent' 933 messages.unable_to_send_email = 'Unable to send email' 934 messages.email_verified = 'Email verified' 935 messages.logged_out = 'Logged out' 936 messages.registration_successful = 'Registration successful' 937 messages.invalid_email = 'Invalid email' 938 messages.unable_send_email = 'Unable to send email' 939 messages.invalid_login = 'Invalid login' 940 messages.invalid_user = 'Invalid user' 941 messages.invalid_password = 'Invalid password' 942 messages.is_empty = "Cannot be empty" 943 messages.mismatched_password = "Password fields don't match" 944 messages.verify_email = \ 945 'Click on the link http://...verify_email/%(key)s to verify your email' 946 messages.verify_email_subject = 'Email verification' 947 messages.username_sent = 'Your username was emailed to you' 948 messages.new_password_sent = 'A new password was emailed to you' 949 messages.password_changed = 'Password changed' 950 messages.retrieve_username = 'Your username is: %(username)s' 951 messages.retrieve_username_subject = 'Username retrieve' 952 messages.retrieve_password = 'Your password is: %(password)s' 953 messages.retrieve_password_subject = 'Password retrieve' 954 messages.reset_password = \ 955 'Click on the link http://...reset_password/%(key)s to reset your password' 956 messages.reset_password_subject = 'Password reset' 957 messages.invalid_reset_password = 'Invalid reset password' 958 messages.profile_updated = 'Profile updated' 959 messages.new_password = 'New password' 960 messages.old_password = 'Old password' 961 messages.group_description = \ 962 'Group uniquely assigned to user %(id)s' 963 964 messages.register_log = 'User %(id)s Registered' 965 messages.login_log = 'User %(id)s Logged-in' 966 messages.login_failed_log = None 967 messages.logout_log = 'User %(id)s Logged-out' 968 messages.profile_log = 'User %(id)s Profile updated' 969 messages.verify_email_log = 'User %(id)s Verification email sent' 970 messages.retrieve_username_log = 'User %(id)s Username retrieved' 971 messages.retrieve_password_log = 'User %(id)s Password retrieved' 972 messages.reset_password_log = 'User %(id)s Password reset' 973 messages.change_password_log = 'User %(id)s Password changed' 974 messages.add_group_log = 'Group %(group_id)s created' 975 messages.del_group_log = 'Group %(group_id)s deleted' 976 messages.add_membership_log = None 977 messages.del_membership_log = None 978 messages.has_membership_log = None 979 messages.add_permission_log = None 980 messages.del_permission_log = None 981 messages.has_permission_log = None 982 messages.impersonate_log = 'User %(id)s is impersonating %(other_id)s' 983 984 messages.label_first_name = 'First name' 985 messages.label_last_name = 'Last name' 986 messages.label_username = 'Username' 987 messages.label_email = 'E-mail' 988 messages.label_password = 'Password' 989 messages.label_registration_key = 'Registration key' 990 messages.label_reset_password_key = 'Reset Password key' 991 messages.label_registration_id = 'Registration identifier' 992 messages.label_role = 'Role' 993 messages.label_description = 'Description' 994 messages.label_user_id = 'User ID' 995 messages.label_group_id = 'Group ID' 996 messages.label_name = 'Name' 997 messages.label_table_name = 'Table name' 998 messages.label_record_id = 'Record ID' 999 messages.label_time_stamp = 'Timestamp' 1000 messages.label_client_ip = 'Client IP' 1001 messages.label_origin = 'Origin' 1002 messages.label_remember_me = "Remember me (for 30 days)" 1003 messages['T'] = current.T 1004 messages.verify_password_comment = 'please input your password again' 1005 messages.lock_keys = True 1006 1007 # for "remember me" option 1008 response = current.response 1009 if auth and auth.remember: #when user wants to be logged in for longer 1010 response.cookies[response.session_id_name]["expires"] = \ 1011 auth.expiration 1012 1013 def lazy_user (auth = self): return auth.user_id 1014 reference_user = 'reference %s' % settings.table_user_name 1015 def represent(id,s=settings): 1016 try: 1017 user = s.table_user(id) 1018 return '%(first_name)s %(last_name)s' % user 1019 except: return id
1020 self.signature = db.Table(self.db,'auth_signature', 1021 Field('is_active','boolean',default=True), 1022 Field('created_on','datetime', 1023 default=request.now, 1024 writable=False,readable=False), 1025 Field('created_by', 1026 reference_user, 1027 default=lazy_user,represent=represent, 1028 writable=False,readable=False, 1029 ), 1030 Field('modified_on','datetime', 1031 update=request.now,default=request.now, 1032 writable=False,readable=False), 1033 Field('modified_by', 1034 reference_user,represent=represent, 1035 default=lazy_user,update=lazy_user, 1036 writable=False,readable=False))
1037 1038 1039
1040 - def _get_user_id(self):
1041 "accessor for auth.user_id" 1042 return self.user and self.user.id or None
1043 user_id = property(_get_user_id, doc="user.id or None") 1044
1045 - def _HTTP(self, *a, **b):
1046 """ 1047 only used in lambda: self._HTTP(404) 1048 """ 1049 1050 raise HTTP(*a, **b)
1051
1052 - def __call__(self):
1053 """ 1054 usage: 1055 1056 def authentication(): return dict(form=auth()) 1057 """ 1058 1059 request = current.request 1060 args = request.args 1061 if not args: 1062 redirect(self.url(args='login',vars=request.vars)) 1063 elif args[0] in self.settings.actions_disabled: 1064 raise HTTP(404) 1065 if args[0] in ('login','logout','register','verify_email', 1066 'retrieve_username','retrieve_password', 1067 'reset_password','request_reset_password', 1068 'change_password','profile','groups', 1069 'impersonate','not_authorized'): 1070 return getattr(self,args[0])() 1071 elif args[0]=='cas' and not self.settings.cas_provider: 1072 if args(1) == 'login': return self.cas_login(version=2) 1073 if args(1) == 'validate': return self.cas_validate(version=2) 1074 if args(1) == 'logout': return self.logout() 1075 else: 1076 raise HTTP(404)
1077
1078 - def navbar(self,prefix='Welcome',action=None):
1079 request = current.request 1080 T = current.T 1081 if isinstance(prefix,str): 1082 prefix = T(prefix) 1083 if not action: 1084 action=URL(request.application,request.controller,'user') 1085 if prefix: 1086 prefix = prefix.strip()+' ' 1087 if self.user_id: 1088 logout=A(T('logout'),_href=action+'/logout') 1089 profile=A(T('profile'),_href=action+'/profile') 1090 password=A(T('password'),_href=action+'/change_password') 1091 bar = SPAN(prefix,self.user.first_name,' [ ', logout, ']',_class='auth_navbar') 1092 if not 'profile' in self.settings.actions_disabled: 1093 bar.insert(4, ' | ') 1094 bar.insert(5, profile) 1095 if not 'change_password' in self.settings.actions_disabled: 1096 bar.insert(-1, ' | ') 1097 bar.insert(-1, password) 1098 else: 1099 login=A(T('login'),_href=action+'/login') 1100 register=A(T('register'),_href=action+'/register') 1101 retrieve_username=A(T('forgot username?'), 1102 _href=action+'/retrieve_username') 1103 lost_password=A(T('lost password?'), 1104 _href=action+'/request_reset_password') 1105 bar = SPAN('[ ',login,' ]',_class='auth_navbar') 1106 1107 if not 'register' in self.settings.actions_disabled: 1108 bar.insert(2, ' | ') 1109 bar.insert(3, register) 1110 if 'username' in self.settings.table_user.fields() and \ 1111 not 'retrieve_username' in self.settings.actions_disabled: 1112 bar.insert(-1, ' | ') 1113 bar.insert(-1, retrieve_username) 1114 if not 'request_reset_password' in self.settings.actions_disabled: 1115 bar.insert(-1, ' | ') 1116 bar.insert(-1, lost_password) 1117 return bar
1118
1119 - def __get_migrate(self, tablename, migrate=True):
1120 1121 if type(migrate).__name__ == 'str': 1122 return (migrate + tablename + '.table') 1123 elif migrate == False: 1124 return False 1125 else: 1126 return True
1127
1128 - def define_tables(self, username=False, migrate=True, fake_migrate=False):
1129 """ 1130 to be called unless tables are defined manually 1131 1132 usages:: 1133 1134 # defines all needed tables and table files 1135 # 'myprefix_auth_user.table', ... 1136 auth.define_tables(migrate='myprefix_') 1137 1138 # defines all needed tables without migration/table files 1139 auth.define_tables(migrate=False) 1140 1141 """ 1142 1143 db = self.db 1144 settings = self.settings 1145 if not settings.table_user_name in db.tables: 1146 passfield = settings.password_field 1147 if username or settings.cas_provider: 1148 table = db.define_table( 1149 settings.table_user_name, 1150 Field('first_name', length=128, default='', 1151 label=self.messages.label_first_name), 1152 Field('last_name', length=128, default='', 1153 label=self.messages.label_last_name), 1154 Field('username', length=128, default='', 1155 label=self.messages.label_username), 1156 Field('email', length=512, default='', 1157 label=self.messages.label_email), 1158 Field(passfield, 'password', length=512, 1159 readable=False, label=self.messages.label_password), 1160 Field('registration_key', length=512, 1161 writable=False, readable=False, default='', 1162 label=self.messages.label_registration_key), 1163 Field('reset_password_key', length=512, 1164 writable=False, readable=False, default='', 1165 label=self.messages.label_reset_password_key), 1166 Field('registration_id', length=512, 1167 writable=False, readable=False, default='', 1168 label=self.messages.label_registration_id), 1169 *settings.extra_fields.get(settings.table_user_name,[]), 1170 **dict( 1171 migrate=self.__get_migrate(settings.table_user_name, 1172 migrate), 1173 fake_migrate=fake_migrate, 1174 format='%(username)s')) 1175 table.username.requires = (IS_MATCH('[\w\.\-]+'), 1176 IS_NOT_IN_DB(db, table.username)) 1177 else: 1178 table = db.define_table( 1179 settings.table_user_name, 1180 Field('first_name', length=128, default='', 1181 label=self.messages.label_first_name), 1182 Field('last_name', length=128, default='', 1183 label=self.messages.label_last_name), 1184 Field('email', length=512, default='', 1185 label=self.messages.label_email), 1186 Field(passfield, 'password', length=512, 1187 readable=False, label=self.messages.label_password), 1188 Field('registration_key', length=512, 1189 writable=False, readable=False, default='', 1190 label=self.messages.label_registration_key), 1191 Field('reset_password_key', length=512, 1192 writable=False, readable=False, default='', 1193 label=self.messages.label_reset_password_key), 1194 *settings.extra_fields.get(settings.table_user_name,[]), 1195 **dict( 1196 migrate=self.__get_migrate(settings.table_user_name, 1197 migrate), 1198 fake_migrate=fake_migrate, 1199 format='%(first_name)s %(last_name)s (%(id)s)')) 1200 table.first_name.requires = \ 1201 IS_NOT_EMPTY(error_message=self.messages.is_empty) 1202 table.last_name.requires = \ 1203 IS_NOT_EMPTY(error_message=self.messages.is_empty) 1204 table[passfield].requires = [CRYPT(key=settings.hmac_key)] 1205 table.email.requires = \ 1206 [IS_EMAIL(error_message=self.messages.invalid_email), 1207 IS_NOT_IN_DB(db, table.email)] 1208 table.registration_key.default = '' 1209 settings.table_user = db[settings.table_user_name] 1210 if not settings.table_group_name in db.tables: 1211 table = db.define_table( 1212 settings.table_group_name, 1213 Field('role', length=512, default='', 1214 label=self.messages.label_role), 1215 Field('description', 'text', 1216 label=self.messages.label_description), 1217 *settings.extra_fields.get(settings.table_group_name,[]), 1218 **dict( 1219 migrate=self.__get_migrate( 1220 settings.table_group_name, migrate), 1221 fake_migrate=fake_migrate, 1222 format = '%(role)s (%(id)s)')) 1223 table.role.requires = IS_NOT_IN_DB(db, '%s.role' 1224 % settings.table_group_name) 1225 settings.table_group = db[settings.table_group_name] 1226 if not settings.table_membership_name in db.tables: 1227 table = db.define_table( 1228 settings.table_membership_name, 1229 Field('user_id', settings.table_user, 1230 label=self.messages.label_user_id), 1231 Field('group_id', settings.table_group, 1232 label=self.messages.label_group_id), 1233 *settings.extra_fields.get(settings.table_membership_name,[]), 1234 **dict( 1235 migrate=self.__get_migrate( 1236 settings.table_membership_name, migrate), 1237 fake_migrate=fake_migrate)) 1238 table.user_id.requires = IS_IN_DB(db, '%s.id' % 1239 settings.table_user_name, 1240 '%(first_name)s %(last_name)s (%(id)s)') 1241 table.group_id.requires = IS_IN_DB(db, '%s.id' % 1242 settings.table_group_name, 1243 '%(role)s (%(id)s)') 1244 settings.table_membership = db[settings.table_membership_name] 1245 if not settings.table_permission_name in db.tables: 1246 table = db.define_table( 1247 settings.table_permission_name, 1248 Field('group_id', settings.table_group, 1249 label=self.messages.label_group_id), 1250 Field('name', default='default', length=512, 1251 label=self.messages.label_name), 1252 Field('table_name', length=512, 1253 label=self.messages.label_table_name), 1254 Field('record_id', 'integer',default=0, 1255 label=self.messages.label_record_id), 1256 *settings.extra_fields.get(settings.table_permission_name,[]), 1257 **dict( 1258 migrate=self.__get_migrate( 1259 settings.table_permission_name, migrate), 1260 fake_migrate=fake_migrate)) 1261 table.group_id.requires = IS_IN_DB(db, '%s.id' % 1262 settings.table_group_name, 1263 '%(role)s (%(id)s)') 1264 table.name.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1265 table.table_name.requires = IS_EMPTY_OR(IS_IN_SET(self.db.tables)) 1266 table.record_id.requires = IS_INT_IN_RANGE(0, 10 ** 9) 1267 settings.table_permission = db[settings.table_permission_name] 1268 if not settings.table_event_name in db.tables: 1269 table = db.define_table( 1270 settings.table_event_name, 1271 Field('time_stamp', 'datetime', 1272 default=current.request.now, 1273 label=self.messages.label_time_stamp), 1274 Field('client_ip', 1275 default=current.request.client, 1276 label=self.messages.label_client_ip), 1277 Field('user_id', settings.table_user, default=None, 1278 label=self.messages.label_user_id), 1279 Field('origin', default='auth', length=512, 1280 label=self.messages.label_origin), 1281 Field('description', 'text', default='', 1282 label=self.messages.label_description), 1283 *settings.extra_fields.get(settings.table_event_name,[]), 1284 **dict( 1285 migrate=self.__get_migrate( 1286 settings.table_event_name, migrate), 1287 fake_migrate=fake_migrate)) 1288 table.user_id.requires = IS_IN_DB(db, '%s.id' % 1289 settings.table_user_name, 1290 '%(first_name)s %(last_name)s (%(id)s)') 1291 table.origin.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1292 table.description.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1293 settings.table_event = db[settings.table_event_name] 1294 now = current.request.now 1295 if settings.cas_domains: 1296 if not settings.table_cas_name in db.tables: 1297 table = db.define_table( 1298 settings.table_cas_name, 1299 Field('user_id', settings.table_user, default=None, 1300 label=self.messages.label_user_id), 1301 Field('created_on','datetime',default=now), 1302 Field('url',requires=IS_URL()), 1303 Field('uuid'), 1304 *settings.extra_fields.get(settings.table_cas_name,[]), 1305 **dict( 1306 migrate=self.__get_migrate( 1307 settings.table_event_name, migrate), 1308 fake_migrate=fake_migrate)) 1309 table.user_id.requires = IS_IN_DB(db, '%s.id' % \ 1310 settings.table_user_name, 1311 '%(first_name)s %(last_name)s (%(id)s)') 1312 settings.table_cas = db[settings.table_cas_name] 1313 if settings.cas_provider: 1314 settings.actions_disabled = \ 1315 ['profile','register','change_password','request_reset_password'] 1316 from gluon.contrib.login_methods.cas_auth import CasAuth 1317 maps = dict((name,lambda v,n=name:v.get(n,None)) for name in \ 1318 settings.table_user.fields if name!='id' \ 1319 and settings.table_user[name].readable) 1320 maps['registration_id'] = \ 1321 lambda v,p=settings.cas_provider:'%s/%s' % (p,v['user']) 1322 settings.login_form = CasAuth( 1323 casversion = 2, 1324 urlbase = settings.cas_provider, 1325 actions=['login','validate','logout'], 1326 maps=maps)
1327 1328
1329 - def log_event(self, description, origin='auth'):
1330 """ 1331 usage:: 1332 1333 auth.log_event(description='this happened', origin='auth') 1334 """ 1335 1336 if self.is_logged_in(): 1337 user_id = self.user.id 1338 else: 1339 user_id = None # user unknown 1340 self.settings.table_event.insert(description=description, 1341 origin=origin, user_id=user_id)
1342
1343 - def get_or_create_user(self, keys):
1344 """ 1345 Used for alternate login methods: 1346 If the user exists already then password is updated. 1347 If the user doesn't yet exist, then they are created. 1348 """ 1349 table_user = self.settings.table_user 1350 if 'registration_id' in table_user.fields() and \ 1351 'registration_id' in keys: 1352 username = 'registration_id' 1353 elif 'username' in table_user.fields(): 1354 username = 'username' 1355 elif 'email' in table_user.fields(): 1356 username = 'email' 1357 else: 1358 raise SyntaxError, "user must have username or email" 1359 passfield = self.settings.password_field 1360 user = self.db(table_user[username] == keys[username]).select().first() 1361 keys['registration_key']='' 1362 if user: 1363 user.update_record(**keys) 1364 else: 1365 if not 'first_name' in keys and 'first_name' in table_user.fields: 1366 keys['first_name']=keys[username] 1367 user_id = table_user.insert(**keys) 1368 user = self.user = table_user[user_id] 1369 if self.settings.create_user_groups: 1370 group_id = self.add_group("user_%s" % user_id) 1371 self.add_membership(group_id, user_id) 1372 return user
1373
1374 - def basic(self):
1375 if not self.settings.allow_basic_login: 1376 return False 1377 basic = current.request.env.http_authorization 1378 if not basic or not basic[:6].lower() == 'basic ': 1379 return False 1380 (username, password) = base64.b64decode(basic[6:]).split(':') 1381 return self.login_bare(username, password)
1382
1383 - def login_bare(self, username, password):
1384 """ 1385 logins user 1386 """ 1387 1388 request = current.request 1389 session = current.session 1390 table_user = self.settings.table_user 1391 if self.settings.login_userfield: 1392 userfield = self.settings.login_userfield 1393 elif 'username' in table_user.fields: 1394 userfield = 'username' 1395 else: 1396 userfield = 'email' 1397 passfield = self.settings.password_field 1398 user = self.db(table_user[userfield] == username).select().first() 1399 password = table_user[passfield].validate(password)[0] 1400 if user: 1401 if not user.registration_key and user[passfield] == password: 1402 user = Storage(table_user._filter_fields(user, id=True)) 1403 session.auth = Storage(user=user, last_visit=request.now, 1404 expiration=self.settings.expiration, 1405 hmac_key = web2py_uuid()) 1406 self.user = user 1407 return user 1408 return False
1409
1410 - def cas_login( 1411 self, 1412 next=DEFAULT, 1413 onvalidation=DEFAULT, 1414 onaccept=DEFAULT, 1415 log=DEFAULT, 1416 version=2, 1417 ):
1418 request, session = current.request, current.session 1419 db, table = self.db, self.settings.table_cas 1420 session._cas_service = request.vars.service or session._cas_service 1421 if not request.env.http_host in self.settings.cas_domains or \ 1422 not session._cas_service: 1423 raise HTTP(403,'not authorized') 1424 def allow_access(): 1425 row = table(url=session._cas_service,user_id=self.user.id) 1426 if row: 1427 row.update_record(created_on=request.now) 1428 uuid = row.uuid 1429 else: 1430 uuid = web2py_uuid() 1431 table.insert(url=session._cas_service, user_id=self.user.id, 1432 uuid=uuid, created_on=request.now) 1433 url = session._cas_service 1434 del session._cas_service 1435 redirect(url+"?ticket="+uuid)
1436 if self.is_logged_in(): 1437 allow_access() 1438 def cas_onaccept(form, onaccept=onaccept): 1439 if onaccept!=DEFAULT: onaccept(form) 1440 allow_access() 1441 return self.login(next,onvalidation,cas_onaccept,log) 1442 1443
1444 - def cas_validate(self,version=2):
1445 request = current.request 1446 db, table = self.db, self.settings.table_cas 1447 current.response.headers['Content-Type']='text' 1448 ticket = table(uuid=request.vars.ticket) 1449 url = request.env.path_info.rsplit('/',1)[0] 1450 if ticket: # and ticket.created_on>request.now-datetime.timedelta(60): 1451 user = self.settings.table_user(ticket.user_id) 1452 fullname = user.first_name+' '+user.last_name 1453 if version==1: 1454 raise HTTP(200,'yes\n%s:%s:%s'%(user.id,user.email,fullname)) 1455 # assume version 2 1456 username = user.get('username',user.email) 1457 raise HTTP(200,'<?xml version="1.0" encoding="UTF-8"?>\n'+\ 1458 TAG['cas:serviceResponse']( 1459 TAG['cas:authenticationSuccess']( 1460 TAG['cas:user'](username), 1461 *[TAG['cas:'+field.name](user[field.name]) \ 1462 for field in self.settings.table_user \ 1463 if field.readable]), 1464 **{'_xmlns:cas':'http://www.yale.edu/tp/cas'}).xml()) 1465 if version==1: 1466 raise HTTP(200,'no\n') 1467 # assume version 2 1468 raise HTTP(200,'<?xml version="1.0" encoding="UTF-8"?>\n'+\ 1469 TAG['cas:serviceResponse']( 1470 TAG['cas:authenticationFailure']( 1471 'Ticket %s not recognized' % ticket, 1472 _code='INVALID TICKET'), 1473 **{'_xmlns:cas':'http://www.yale.edu/tp/cas'}).xml())
1474 1475
1476 - def login( 1477 self, 1478 next=DEFAULT, 1479 onvalidation=DEFAULT, 1480 onaccept=DEFAULT, 1481 log=DEFAULT, 1482 ):
1483 """ 1484 returns a login form 1485 1486 .. method:: Auth.login([next=DEFAULT [, onvalidation=DEFAULT 1487 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1488 1489 """ 1490 1491 table_user = self.settings.table_user 1492 if self.settings.login_userfield: 1493 username = self.settings.login_userfield 1494 elif 'username' in table_user.fields: 1495 username = 'username' 1496 else: 1497 username = 'email' 1498 if 'username' in table_user.fields or not self.settings.login_email_validate: 1499 tmpvalidator = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1500 else: 1501 tmpvalidator = IS_EMAIL(error_message=self.messages.invalid_email) 1502 old_requires = table_user[username].requires 1503 table_user[username].requires = tmpvalidator 1504 1505 request = current.request 1506 response = current.response 1507 session = current.session 1508 1509 passfield = self.settings.password_field 1510 if next == DEFAULT: 1511 next = request.get_vars._next \ 1512 or request.post_vars._next \ 1513 or self.settings.login_next 1514 if onvalidation == DEFAULT: 1515 onvalidation = self.settings.login_onvalidation 1516 if onaccept == DEFAULT: 1517 onaccept = self.settings.login_onaccept 1518 if log == DEFAULT: 1519 log = self.messages.login_log 1520 1521 user = None # default 1522 1523 # do we use our own login form, or from a central source? 1524 if self.settings.login_form == self: 1525 form = SQLFORM( 1526 table_user, 1527 fields=[username, passfield], 1528 hidden=dict(_next=next), 1529 showid=self.settings.showid, 1530 submit_button=self.messages.submit_button, 1531 delete_label=self.messages.delete_label, 1532 formstyle=self.settings.formstyle 1533 ) 1534 1535 if self.settings.remember_me_form: 1536 ## adds a new input checkbox "remember me for longer" 1537 addrow(form,XML("&nbsp;"), 1538 DIV(XML("&nbsp;"), 1539 INPUT(_type='checkbox', 1540 _class='checkbox', 1541 _id="auth_user_remember", 1542 _name="remember", 1543 ), 1544 XML("&nbsp;&nbsp;"), 1545 LABEL( 1546 self.messages.label_remember_me, 1547 _for="auth_user_remember", 1548 )),"", 1549 self.settings.formstyle, 1550 'auth_user_remember__row') 1551 1552 captcha = self.settings.login_captcha or \ 1553 (self.settings.login_captcha!=False and self.settings.captcha) 1554 if captcha: 1555 addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') 1556 accepted_form = False 1557 1558 if form.accepts(request, session, 1559 formname='login', dbio=False, 1560 onvalidation=onvalidation, 1561 hideerror=self.settings.hideerror): 1562 1563 accepted_form = True 1564 # check for username in db 1565 user = self.db(table_user[username] == form.vars[username]).select().first() 1566 if user: 1567 # user in db, check if registration pending or disabled 1568 temp_user = user 1569 if temp_user.registration_key == 'pending': 1570 response.flash = self.messages.registration_pending 1571 return form 1572 elif temp_user.registration_key in ('disabled','blocked'): 1573 response.flash = self.messages.login_disabled 1574 return form 1575 elif temp_user.registration_key!=None and \ 1576 temp_user.registration_key.strip(): 1577 response.flash = \ 1578 self.messages.registration_verifying 1579 return form 1580 # try alternate logins 1st as these have the 1581 # current version of the password 1582 user = None 1583 for login_method in self.settings.login_methods: 1584 if login_method != self and \ 1585 login_method(request.vars[username], 1586 request.vars[passfield]): 1587 if not self in self.settings.login_methods: 1588 # do not store password in db 1589 form.vars[passfield] = None 1590 user = self.get_or_create_user(form.vars) 1591 break 1592 if not user: 1593 # alternates have failed, maybe because service inaccessible 1594 if self.settings.login_methods[0] == self: 1595 # try logging in locally using cached credentials 1596 if temp_user[passfield] == form.vars.get(passfield, ''): 1597 # success 1598 user = temp_user 1599 else: 1600 # user not in db 1601 if not self.settings.alternate_requires_registration: 1602 # we're allowed to auto-register users from external systems 1603 for login_method in self.settings.login_methods: 1604 if login_method != self and \ 1605 login_method(request.vars[username], 1606 request.vars[passfield]): 1607 if not self in self.settings.login_methods: 1608 # do not store password in db 1609 form.vars[passfield] = None 1610 user = self.get_or_create_user(form.vars) 1611 break 1612 if not user: 1613 if self.settings.login_failed_log: 1614 self.log_event(self.settings.login_failed_log % request.post_vars) 1615 # invalid login 1616 session.flash = self.messages.invalid_login 1617 redirect(self.url(args=request.args,vars=request.get_vars)) 1618 1619 else: 1620 # use a central authentication server 1621 cas = self.settings.login_form 1622 cas_user = cas.get_user() 1623 1624 if cas_user: 1625 cas_user[passfield] = None 1626 user = self.get_or_create_user(table_user._filter_fields(cas_user)) 1627 elif hasattr(cas,'login_form'): 1628 return cas.login_form() 1629 else: 1630 # we need to pass through login again before going on 1631 next = self.url('user',args='login',vars=dict(_next=next)) 1632 redirect(cas.login_url(next)) 1633 1634 1635 # process authenticated users 1636 if user: 1637 user = Storage(table_user._filter_fields(user, id=True)) 1638 1639 if request.vars.has_key("remember"): 1640 # user wants to be logged in for longer 1641 session.auth = Storage( 1642 user = user, 1643 last_visit = request.now, 1644 expiration = self.settings.long_expiration, 1645 remember = True, 1646 hmac_key = web2py_uuid() 1647 ) 1648 else: 1649 # user doesn't want to be logged in for longer 1650 session.auth = Storage( 1651 user = user, 1652 last_visit = request.now, 1653 expiration = self.settings.expiration, 1654 remember = False, 1655 hmac_key = web2py_uuid() 1656 ) 1657 1658 self.user = user 1659 callback(onaccept,None) 1660 session.flash = self.messages.logged_in 1661 if log and self.user: 1662 self.log_event(log % self.user) 1663 1664 # how to continue 1665 if self.settings.login_form == self: 1666 if accepted_form: 1667 callback(onaccept,form) 1668 if isinstance(next, (list, tuple)): 1669 # fix issue with 2.6 1670 next = next[0] 1671 if next and not next[0] == '/' and next[:4] != 'http': 1672 next = self.url(next.replace('[id]', str(form.vars.id))) 1673 redirect(next) 1674 table_user[username].requires = old_requires 1675 return form 1676 else: 1677 redirect(next)
1678
1679 - def logout(self, next=DEFAULT, onlogout=DEFAULT, log=DEFAULT):
1680 """ 1681 logout and redirects to login 1682 1683 .. method:: Auth.logout ([next=DEFAULT[, onlogout=DEFAULT[, 1684 log=DEFAULT]]]) 1685 1686 """ 1687 1688 if next == DEFAULT: 1689 next = self.settings.logout_next 1690 if onlogout == DEFAULT: 1691 onlogout = self.settings.logout_onlogout 1692 if onlogout: 1693 onlogout(self.user) 1694 if log == DEFAULT: 1695 log = self.messages.logout_log 1696 if log and self.user: 1697 self.log_event(log % self.user) 1698 1699 if self.settings.login_form != self: 1700 cas = self.settings.login_form 1701 cas_user = cas.get_user() 1702 if cas_user: 1703 next = cas.logout_url(next) 1704 1705 current.session.auth = None 1706 current.session.flash = self.messages.logged_out 1707 if next: 1708 redirect(next)
1709
1710 - def register( 1711 self, 1712 next=DEFAULT, 1713 onvalidation=DEFAULT, 1714 onaccept=DEFAULT, 1715 log=DEFAULT, 1716 ):
1717 """ 1718 returns a registration form 1719 1720 .. method:: Auth.register([next=DEFAULT [, onvalidation=DEFAULT 1721 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1722 1723 """ 1724 1725 table_user = self.settings.table_user 1726 request = current.request 1727 response = current.response 1728 session = current.session 1729 if self.is_logged_in(): 1730 redirect(self.settings.logged_url) 1731 if next == DEFAULT: 1732 next = request.get_vars._next \ 1733 or request.post_vars._next \ 1734 or self.settings.register_next 1735 if onvalidation == DEFAULT: 1736 onvalidation = self.settings.register_onvalidation 1737 if onaccept == DEFAULT: 1738 onaccept = self.settings.register_onaccept 1739 if log == DEFAULT: 1740 log = self.messages.register_log 1741 1742 passfield = self.settings.password_field 1743 formstyle = self.settings.formstyle 1744 form = SQLFORM(table_user, 1745 fields = self.settings.register_fields, 1746 hidden=dict(_next=next), 1747 showid=self.settings.showid, 1748 submit_button=self.messages.submit_button, 1749 delete_label=self.messages.delete_label, 1750 formstyle=formstyle 1751 ) 1752 for i, row in enumerate(form[0].components): 1753 item = row.element('input',_name=passfield) 1754 if item: 1755 form.custom.widget.password_two = \ 1756 INPUT(_name="password_two", _type="password", 1757 requires=IS_EXPR('value==%s' % \ 1758 repr(request.vars.get(passfield, None)), 1759 error_message=self.messages.mismatched_password)) 1760 1761 addrow(form, self.messages.verify_password + ':', 1762 form.custom.widget.password_two, 1763 self.messages.verify_password_comment, 1764 formstyle, 1765 '%s_%s__row' % (table_user, 'password_two'), 1766 position=i+1) 1767 break 1768 captcha = self.settings.register_captcha or self.settings.captcha 1769 if captcha: 1770 addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') 1771 1772 table_user.registration_key.default = key = web2py_uuid() 1773 if form.accepts(request, session, formname='register', 1774 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1775 description = self.messages.group_description % form.vars 1776 if self.settings.create_user_groups: 1777 group_id = self.add_group("user_%s" % form.vars.id, description) 1778 self.add_membership(group_id, form.vars.id) 1779 if self.settings.registration_requires_verification: 1780 if not self.settings.mailer or \ 1781 not self.settings.mailer.send(to=form.vars.email, 1782 subject=self.messages.verify_email_subject, 1783 message=self.messages.verify_email 1784 % dict(key=key)): 1785 self.db.rollback() 1786 response.flash = self.messages.unable_send_email 1787 return form 1788 session.flash = self.messages.email_sent 1789 elif self.settings.registration_requires_approval: 1790 table_user[form.vars.id] = dict(registration_key='pending') 1791 session.flash = self.messages.registration_pending 1792 else: 1793 table_user[form.vars.id] = dict(registration_key='') 1794 session.flash = self.messages.registration_successful 1795 table_user = self.settings.table_user 1796 if 'username' in table_user.fields: 1797 username = 'username' 1798 else: 1799 username = 'email' 1800 user = self.db(table_user[username] == form.vars[username]).select().first() 1801 user = Storage(table_user._filter_fields(user, id=True)) 1802 session.auth = Storage(user=user, last_visit=request.now, 1803 expiration=self.settings.expiration, 1804 hmac_key = web2py_uuid()) 1805 self.user = user 1806 session.flash = self.messages.logged_in 1807 if log: 1808 self.log_event(log % form.vars) 1809 callback(onaccept,form) 1810 if not next: 1811 next = self.url(args = request.args) 1812 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1813 next = next[0] 1814 elif next and not next[0] == '/' and next[:4] != 'http': 1815 next = self.url(next.replace('[id]', str(form.vars.id))) 1816 redirect(next) 1817 return form
1818
1819 - def is_logged_in(self):
1820 """ 1821 checks if the user is logged in and returns True/False. 1822 if so user is in auth.user as well as in session.auth.user 1823 """ 1824 1825 if self.user: 1826 return True 1827 return False
1828
1829 - def verify_email( 1830 self, 1831 next=DEFAULT, 1832 onaccept=DEFAULT, 1833 log=DEFAULT, 1834 ):
1835 """ 1836 action user to verify the registration email, XXXXXXXXXXXXXXXX 1837 1838 .. method:: Auth.verify_email([next=DEFAULT [, onvalidation=DEFAULT 1839 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1840 1841 """ 1842 1843 key = current.request.args[-1] 1844 table_user = self.settings.table_user 1845 user = self.db(table_user.registration_key == key).select().first() 1846 if not user: 1847 raise HTTP(404) 1848 if self.settings.registration_requires_approval: 1849 user.update_record(registration_key = 'pending') 1850 current.session.flash = self.messages.registration_pending 1851 else: 1852 user.update_record(registration_key = '') 1853 current.session.flash = self.messages.email_verified 1854 if log == DEFAULT: 1855 log = self.messages.verify_email_log 1856 if next == DEFAULT: 1857 next = self.settings.verify_email_next 1858 if onaccept == DEFAULT: 1859 onaccept = self.settings.verify_email_onaccept 1860 if log: 1861 self.log_event(log % user) 1862 callback(onaccept,user) 1863 redirect(next)
1864
1865 - def retrieve_username( 1866 self, 1867 next=DEFAULT, 1868 onvalidation=DEFAULT, 1869 onaccept=DEFAULT, 1870 log=DEFAULT, 1871 ):
1872 """ 1873 returns a form to retrieve the user username 1874 (only if there is a username field) 1875 1876 .. method:: Auth.retrieve_username([next=DEFAULT 1877 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1878 1879 """ 1880 1881 table_user = self.settings.table_user 1882 if not 'username' in table_user.fields: 1883 raise HTTP(404) 1884 request = current.request 1885 response = current.response 1886 session = current.session 1887 captcha = self.settings.retrieve_username_captcha or \ 1888 (self.settings.retrieve_username_captcha!=False and self.settings.captcha) 1889 if not self.settings.mailer: 1890 response.flash = self.messages.function_disabled 1891 return '' 1892 if next == DEFAULT: 1893 next = request.get_vars._next \ 1894 or request.post_vars._next \ 1895 or self.settings.retrieve_username_next 1896 if onvalidation == DEFAULT: 1897 onvalidation = self.settings.retrieve_username_onvalidation 1898 if onaccept == DEFAULT: 1899 onaccept = self.settings.retrieve_username_onaccept 1900 if log == DEFAULT: 1901 log = self.messages.retrieve_username_log 1902 old_requires = table_user.email.requires 1903 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 1904 error_message=self.messages.invalid_email)] 1905 form = SQLFORM(table_user, 1906 fields=['email'], 1907 hidden=dict(_next=next), 1908 showid=self.settings.showid, 1909 submit_button=self.messages.submit_button, 1910 delete_label=self.messages.delete_label, 1911 formstyle=self.settings.formstyle 1912 ) 1913 if captcha: 1914 addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') 1915 1916 if form.accepts(request, session, 1917 formname='retrieve_username', dbio=False, 1918 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1919 user = self.db(table_user.email == form.vars.email).select().first() 1920 if not user: 1921 current.session.flash = \ 1922 self.messages.invalid_email 1923 redirect(self.url(args=request.args)) 1924 username = user.username 1925 self.settings.mailer.send(to=form.vars.email, 1926 subject=self.messages.retrieve_username_subject, 1927 message=self.messages.retrieve_username 1928 % dict(username=username)) 1929 session.flash = self.messages.email_sent 1930 if log: 1931 self.log_event(log % user) 1932 callback(onaccept,form) 1933 if not next: 1934 next = self.url(args = request.args) 1935 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1936 next = next[0] 1937 elif next and not next[0] == '/' and next[:4] != 'http': 1938 next = self.url(next.replace('[id]', str(form.vars.id))) 1939 redirect(next) 1940 table_user.email.requires = old_requires 1941 return form
1942
1943 - def random_password(self):
1944 import string 1945 import random 1946 password = '' 1947 specials=r'!#$*' 1948 for i in range(0,3): 1949 password += random.choice(string.lowercase) 1950 password += random.choice(string.uppercase) 1951 password += random.choice(string.digits) 1952 password += random.choice(specials) 1953 return ''.join(random.sample(password,len(password)))
1954
1955 - def reset_password_deprecated( 1956 self, 1957 next=DEFAULT, 1958 onvalidation=DEFAULT, 1959 onaccept=DEFAULT, 1960 log=DEFAULT, 1961 ):
1962 """ 1963 returns a form to reset the user password (deprecated) 1964 1965 .. method:: Auth.reset_password_deprecated([next=DEFAULT 1966 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1967 1968 """ 1969 1970 table_user = self.settings.table_user 1971 request = current.request 1972 response = current.response 1973 session = current.session 1974 if not self.settings.mailer: 1975 response.flash = self.messages.function_disabled 1976 return '' 1977 if next == DEFAULT: 1978 next = request.get_vars._next \ 1979 or request.post_vars._next \ 1980 or self.settings.retrieve_password_next 1981 if onvalidation == DEFAULT: 1982 onvalidation = self.settings.retrieve_password_onvalidation 1983 if onaccept == DEFAULT: 1984 onaccept = self.settings.retrieve_password_onaccept 1985 if log == DEFAULT: 1986 log = self.messages.retrieve_password_log 1987 old_requires = table_user.email.requires 1988 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 1989 error_message=self.messages.invalid_email)] 1990 form = SQLFORM(table_user, 1991 fields=['email'], 1992 hidden=dict(_next=next), 1993 showid=self.settings.showid, 1994 submit_button=self.messages.submit_button, 1995 delete_label=self.messages.delete_label, 1996 formstyle=self.settings.formstyle 1997 ) 1998 if form.accepts(request, session, 1999 formname='retrieve_password', dbio=False, 2000 onvalidation=onvalidation,hideerror=self.settings.hideerror): 2001 user = self.db(table_user.email == form.vars.email).select().first() 2002 if not user: 2003 current.session.flash = \ 2004 self.messages.invalid_email 2005 redirect(self.url(args=request.args)) 2006 elif user.registration_key in ('pending','disabled','blocked'): 2007 current.session.flash = \ 2008 self.messages.registration_pending 2009 redirect(self.url(args=request.args)) 2010 password = self.random_password() 2011 passfield = self.settings.password_field 2012 d = {passfield: table_user[passfield].validate(password)[0], 2013 'registration_key': ''} 2014 user.update_record(**d) 2015 if self.settings.mailer and \ 2016 self.settings.mailer.send(to=form.vars.email, 2017 subject=self.messages.retrieve_password_subject, 2018 message=self.messages.retrieve_password \ 2019 % dict(password=password)): 2020 session.flash = self.messages.email_sent 2021 else: 2022 session.flash = self.messages.unable_to_send_email 2023 if log: 2024 self.log_event(log % user) 2025 callback(onaccept,form) 2026 if not next: 2027 next = self.url(args = request.args) 2028 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2029 next = next[0] 2030 elif next and not next[0] == '/' and next[:4] != 'http': 2031 next = self.url(next.replace('[id]', str(form.vars.id))) 2032 redirect(next) 2033 table_user.email.requires = old_requires 2034 return form
2035
2036 - def reset_password( 2037 self, 2038 next=DEFAULT, 2039 onvalidation=DEFAULT, 2040 onaccept=DEFAULT, 2041 log=DEFAULT, 2042 ):
2043 """ 2044 returns a form to reset the user password 2045 2046 .. method:: Auth.reset_password([next=DEFAULT 2047 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2048 2049 """ 2050 2051 table_user = self.settings.table_user 2052 request = current.request 2053 # response = current.response 2054 session = current.session 2055 2056 if next == DEFAULT: 2057 next = request.get_vars._next \ 2058 or request.post_vars._next \ 2059 or self.settings.reset_password_next 2060 2061 try: 2062 key = request.vars.key or request.args[-1] 2063 t0 = int(key.split('-')[0]) 2064 if time.time()-t0 > 60*60*24: raise Exception 2065 user = self.db(table_user.reset_password_key == key).select().first() 2066 if not user: raise Exception 2067 except Exception: 2068 session.flash = self.messages.invalid_reset_password 2069 redirect(next) 2070 passfield = self.settings.password_field 2071 form = SQLFORM.factory( 2072 Field('new_password', 'password', 2073 label=self.messages.new_password, 2074 requires=self.settings.table_user[passfield].requires), 2075 Field('new_password2', 'password', 2076 label=self.messages.verify_password, 2077 requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), 2078 self.messages.mismatched_password)]), 2079 submit_button=self.messages.submit_button, 2080 formstyle=self.settings.formstyle, 2081 ) 2082 if form.accepts(request,session,hideerror=self.settings.hideerror): 2083 user.update_record(**{passfield:form.vars.new_password, 2084 'registration_key':'', 2085 'reset_password_key':''}) 2086 session.flash = self.messages.password_changed 2087 redirect(next) 2088 return form
2089
2090 - def request_reset_password( 2091 self, 2092 next=DEFAULT, 2093 onvalidation=DEFAULT, 2094 onaccept=DEFAULT, 2095 log=DEFAULT, 2096 ):
2097 """ 2098 returns a form to reset the user password 2099 2100 .. method:: Auth.reset_password([next=DEFAULT 2101 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2102 2103 """ 2104 2105 table_user = self.settings.table_user 2106 request = current.request 2107 response = current.response 2108 session = current.session 2109 captcha = self.settings.retrieve_password_captcha or \ 2110 (self.settings.retrieve_password_captcha!=False and self.settings.captcha) 2111 2112 if next == DEFAULT: 2113 next = request.get_vars._next \ 2114 or request.post_vars._next \ 2115 or self.settings.request_reset_password_next 2116 2117 if not self.settings.mailer: 2118 response.flash = self.messages.function_disabled 2119 return '' 2120 if onvalidation == DEFAULT: 2121 onvalidation = self.settings.reset_password_onvalidation 2122 if onaccept == DEFAULT: 2123 onaccept = self.settings.reset_password_onaccept 2124 if log == DEFAULT: 2125 log = self.messages.reset_password_log 2126 # old_requires = table_user.email.requires <<< perhaps should be restored 2127 table_user.email.requires = [ 2128 IS_EMAIL(error_message=self.messages.invalid_email), 2129 IS_IN_DB(self.db, table_user.email, 2130 error_message=self.messages.invalid_email)] 2131 form = SQLFORM(table_user, 2132 fields=['email'], 2133 hidden=dict(_next=next), 2134 showid=self.settings.showid, 2135 submit_button=self.messages.submit_button, 2136 delete_label=self.messages.delete_label, 2137 formstyle=self.settings.formstyle 2138 ) 2139 if captcha: 2140 addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') 2141 if form.accepts(request, session, 2142 formname='reset_password', dbio=False, 2143 onvalidation=onvalidation, 2144 hideerror=self.settings.hideerror): 2145 user = self.db(table_user.email == form.vars.email).select().first() 2146 if not user: 2147 session.flash = self.messages.invalid_email 2148 redirect(self.url(args=request.args)) 2149 elif user.registration_key in ('pending','disabled','blocked'): 2150 session.flash = self.messages.registration_pending 2151 redirect(self.url(args=request.args)) 2152 reset_password_key = str(int(time.time()))+'-' + web2py_uuid() 2153 2154 if self.settings.mailer.send(to=form.vars.email, 2155 subject=self.messages.reset_password_subject, 2156 message=self.messages.reset_password % \ 2157 dict(key=reset_password_key)): 2158 session.flash = self.messages.email_sent 2159 user.update_record(reset_password_key=reset_password_key) 2160 else: 2161 session.flash = self.messages.unable_to_send_email 2162 if log: 2163 self.log_event(log % user) 2164 callback(onaccept,form) 2165 if not next: 2166 next = self.url(args = request.args) 2167 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2168 next = next[0] 2169 elif next and not next[0] == '/' and next[:4] != 'http': 2170 next = self.url(next.replace('[id]', str(form.vars.id))) 2171 redirect(next) 2172 # old_requires = table_user.email.requires 2173 return form
2174
2175 - def retrieve_password( 2176 self, 2177 next=DEFAULT, 2178 onvalidation=DEFAULT, 2179 onaccept=DEFAULT, 2180 log=DEFAULT, 2181 ):
2182 if self.settings.reset_password_requires_verification: 2183 return self.request_reset_password(next,onvalidation,onaccept,log) 2184 else: 2185 return self.reset_password_deprecated(next,onvalidation,onaccept,log)
2186
2187 - def change_password( 2188 self, 2189 next=DEFAULT, 2190 onvalidation=DEFAULT, 2191 onaccept=DEFAULT, 2192 log=DEFAULT, 2193 ):
2194 """ 2195 returns a form that lets the user change password 2196 2197 .. method:: Auth.change_password([next=DEFAULT[, onvalidation=DEFAULT[, 2198 onaccept=DEFAULT[, log=DEFAULT]]]]) 2199 """ 2200 2201 if not self.is_logged_in(): 2202 redirect(self.settings.login_url) 2203 db = self.db 2204 table_user = self.settings.table_user 2205 usern = self.settings.table_user_name 2206 s = db(table_user.id == self.user.id) 2207 2208 request = current.request 2209 session = current.session 2210 if next == DEFAULT: 2211 next = request.get_vars._next \ 2212 or request.post_vars._next \ 2213 or self.settings.change_password_next 2214 if onvalidation == DEFAULT: 2215 onvalidation = self.settings.change_password_onvalidation 2216 if onaccept == DEFAULT: 2217 onaccept = self.settings.change_password_onaccept 2218 if log == DEFAULT: 2219 log = self.messages.change_password_log 2220 passfield = self.settings.password_field 2221 form = SQLFORM.factory( 2222 Field('old_password', 'password', 2223 label=self.messages.old_password, 2224 requires=validators( 2225 table_user[passfield].requires, 2226 IS_IN_DB(s, '%s.%s' % (usern, passfield), 2227 error_message=self.messages.invalid_password))), 2228 Field('new_password', 'password', 2229 label=self.messages.new_password, 2230 requires=table_user[passfield].requires), 2231 Field('new_password2', 'password', 2232 label=self.messages.verify_password, 2233 requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), 2234 self.messages.mismatched_password)]), 2235 submit_button=self.messages.submit_button, 2236 formstyle = self.settings.formstyle 2237 ) 2238 if form.accepts(request, session, 2239 formname='change_password', 2240 onvalidation=onvalidation, 2241 hideerror=self.settings.hideerror): 2242 d = {passfield: form.vars.new_password} 2243 s.update(**d) 2244 session.flash = self.messages.password_changed 2245 if log: 2246 self.log_event(log % self.user) 2247 callback(onaccept,form) 2248 if not next: 2249 next = self.url(args=request.args) 2250 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2251 next = next[0] 2252 elif next and not next[0] == '/' and next[:4] != 'http': 2253 next = self.url(next.replace('[id]', str(form.vars.id))) 2254 redirect(next) 2255 return form
2256
2257 - def profile( 2258 self, 2259 next=DEFAULT, 2260 onvalidation=DEFAULT, 2261 onaccept=DEFAULT, 2262 log=DEFAULT, 2263 ):
2264 """ 2265 returns a form that lets the user change his/her profile 2266 2267 .. method:: Auth.profile([next=DEFAULT [, onvalidation=DEFAULT 2268 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2269 2270 """ 2271 2272 table_user = self.settings.table_user 2273 if not self.is_logged_in(): 2274 redirect(self.settings.login_url) 2275 passfield = self.settings.password_field 2276 self.settings.table_user[passfield].writable = False 2277 request = current.request 2278 session = current.session 2279 if next == DEFAULT: 2280 next = request.get_vars._next \ 2281 or request.post_vars._next \ 2282 or self.settings.profile_next 2283 if onvalidation == DEFAULT: 2284 onvalidation = self.settings.profile_onvalidation 2285 if onaccept == DEFAULT: 2286 onaccept = self.settings.profile_onaccept 2287 if log == DEFAULT: 2288 log = self.messages.profile_log 2289 form = SQLFORM( 2290 table_user, 2291 self.user.id, 2292 fields = self.settings.profile_fields, 2293 hidden = dict(_next=next), 2294 showid = self.settings.showid, 2295 submit_button = self.messages.submit_button, 2296 delete_label = self.messages.delete_label, 2297 upload = self.settings.download_url, 2298 formstyle = self.settings.formstyle 2299 ) 2300 if form.accepts(request, session, 2301 formname='profile', 2302 onvalidation=onvalidation,hideerror=self.settings.hideerror): 2303 self.user.update(table_user._filter_fields(form.vars)) 2304 session.flash = self.messages.profile_updated 2305 if log: 2306 self.log_event(log % self.user) 2307 callback(onaccept,form) 2308 if not next: 2309 next = self.url(args=request.args) 2310 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2311 next = next[0] 2312 elif next and not next[0] == '/' and next[:4] != 'http': 2313 next = self.url(next.replace('[id]', str(form.vars.id))) 2314 redirect(next) 2315 return form
2316
2317 - def is_impersonating(self):
2318 return current.session.auth.impersonator
2319
2320 - def impersonate(self, user_id=DEFAULT):
2321 """ 2322 usage: POST TO http://..../impersonate request.post_vars.user_id=<id> 2323 set request.post_vars.user_id to 0 to restore original user. 2324 2325 requires impersonator is logged in and 2326 has_permission('impersonate', 'auth_user', user_id) 2327 """ 2328 request = current.request 2329 session = current.session 2330 auth = session.auth 2331 if not self.is_logged_in(): 2332 raise HTTP(401, "Not Authorized") 2333 current_id = auth.user.id 2334 requested_id = user_id 2335 if user_id == DEFAULT: 2336 user_id = current.request.post_vars.user_id 2337 if user_id and user_id != self.user.id and user_id != '0': 2338 if not self.has_permission('impersonate', 2339 self.settings.table_user_name, 2340 user_id): 2341 raise HTTP(403, "Forbidden") 2342 user = self.settings.table_user(user_id) 2343 if not user: 2344 raise HTTP(401, "Not Authorized") 2345 auth.impersonator = cPickle.dumps(session) 2346 auth.user.update( 2347 self.settings.table_user._filter_fields(user, True)) 2348 self.user = auth.user 2349 if self.settings.login_onaccept: 2350 form = Storage(dict(vars=self.user)) 2351 self.settings.login_onaccept(form) 2352 log = self.messages.impersonate_log 2353 if log: 2354 self.log_event(log % dict(id=current_id,other_id=auth.user.id)) 2355 elif user_id in (0, '0') and self.is_impersonating(): 2356 session.clear() 2357 session.update(cPickle.loads(auth.impersonator)) 2358 self.user = session.auth.user 2359 if requested_id == DEFAULT and not request.post_vars: 2360 return SQLFORM.factory(Field('user_id','integer')) 2361 return self.user
2362
2363 - def groups(self):
2364 """ 2365 displays the groups and their roles for the logged in user 2366 """ 2367 2368 if not self.is_logged_in(): 2369 redirect(self.settings.login_url) 2370 memberships = self.db(self.settings.table_membership.user_id 2371 == self.user.id).select() 2372 table = TABLE() 2373 for membership in memberships: 2374 groups = self.db(self.settings.table_group.id 2375 == membership.group_id).select() 2376 if groups: 2377 group = groups[0] 2378 table.append(TR(H3(group.role, '(%s)' % group.id))) 2379 table.append(TR(P(group.description))) 2380 if not memberships: 2381 return None 2382 return table
2383
2384 - def not_authorized(self):
2385 """ 2386 you can change the view for this page to make it look as you like 2387 """ 2388 2389 return 'ACCESS DENIED'
2390
2391 - def requires(self, condition):
2392 """ 2393 decorator that prevents access to action if not logged in 2394 """ 2395 2396 def decorator(action): 2397 2398 def f(*a, **b): 2399 2400 if self.settings.allow_basic_login_only and not self.basic(): 2401 if current.request.is_restful: 2402 raise HTTP(403,"Not authorized") 2403 return call_or_redirect(self.settings.on_failed_authorization) 2404 2405 if not condition: 2406 if current.request.is_restful: 2407 raise HTTP(403,"Not authorized") 2408 if not self.basic() and not self.is_logged_in(): 2409 request = current.request 2410 next = URL(r=request,args=request.args, 2411 vars=request.get_vars) 2412 current.session.flash = current.response.flash 2413 return call_or_redirect( 2414 self.settings.on_failed_authentication, 2415 self.settings.login_url + '?_next='+urllib.quote(next)) 2416 else: 2417 current.session.flash = self.messages.access_denied 2418 return call_or_redirect(self.settings.on_failed_authorization) 2419 return action(*a, **b)
2420 f.__doc__ = action.__doc__ 2421 f.__name__ = action.__name__ 2422 f.__dict__.update(action.__dict__) 2423 return f 2424 2425 return decorator 2426
2427 - def requires_login(self):
2428 """ 2429 decorator that prevents access to action if not logged in 2430 """ 2431 2432 def decorator(action): 2433 2434 def f(*a, **b): 2435 2436 if self.settings.allow_basic_login_only and not self.basic(): 2437 if current.request.is_restful: 2438 raise HTTP(403,"Not authorized") 2439 return call_or_redirect(self.settings.on_failed_authorization) 2440 2441 if not self.basic() and not self.is_logged_in(): 2442 if current.request.is_restful: 2443 raise HTTP(403,"Not authorized") 2444 request = current.request 2445 next = URL(r=request,args=request.args, 2446 vars=request.get_vars) 2447 current.session.flash = current.response.flash 2448 return call_or_redirect( 2449 self.settings.on_failed_authentication, 2450 self.settings.login_url + '?_next='+urllib.quote(next) 2451 ) 2452 return action(*a, **b)
2453 f.__doc__ = action.__doc__ 2454 f.__name__ = action.__name__ 2455 f.__dict__.update(action.__dict__) 2456 return f 2457 2458 return decorator 2459
2460 - def requires_membership(self, role=None, group_id=None):
2461 """ 2462 decorator that prevents access to action if not logged in or 2463 if user logged in is not a member of group_id. 2464 If role is provided instead of group_id then the 2465 group_id is calculated. 2466 """ 2467 2468 def decorator(action): 2469 def f(*a, **b): 2470 if self.settings.allow_basic_login_only and not self.basic(): 2471 if current.request.is_restful: 2472 raise HTTP(403,"Not authorized") 2473 return call_or_redirect(self.settings.on_failed_authorization) 2474 2475 if not self.basic() and not self.is_logged_in(): 2476 if current.request.is_restful: 2477 raise HTTP(403,"Not authorized") 2478 request = current.request 2479 next = URL(r=request,args=request.args, 2480 vars=request.get_vars) 2481 current.session.flash = current.response.flash 2482 return call_or_redirect( 2483 self.settings.on_failed_authentication, 2484 self.settings.login_url + '?_next='+urllib.quote(next) 2485 ) 2486 if not self.has_membership(group_id=group_id, role=role): 2487 current.session.flash = self.messages.access_denied 2488 return call_or_redirect(self.settings.on_failed_authorization) 2489 return action(*a, **b)
2490 f.__doc__ = action.__doc__ 2491 f.__name__ = action.__name__ 2492 f.__dict__.update(action.__dict__) 2493 return f 2494 2495 return decorator 2496 2497
2498 - def requires_permission( 2499 self, 2500 name, 2501 table_name='', 2502 record_id=0, 2503 ):
2504 """ 2505 decorator that prevents access to action if not logged in or 2506 if user logged in is not a member of any group (role) that 2507 has 'name' access to 'table_name', 'record_id'. 2508 """ 2509 2510 def decorator(action): 2511 2512 def f(*a, **b): 2513 if self.settings.allow_basic_login_only and not self.basic(): 2514 if current.request.is_restful: 2515 raise HTTP(403,"Not authorized") 2516 return call_or_redirect(self.settings.on_failed_authorization) 2517 2518 if not self.basic() and not self.is_logged_in(): 2519 if current.request.is_restful: 2520 raise HTTP(403,"Not authorized") 2521 request = current.request 2522 next = URL(r=request,args=request.args, 2523 vars=request.get_vars) 2524 current.session.flash = current.response.flash 2525 return call_or_redirect( 2526 self.settings.on_failed_authentication, 2527 self.settings.login_url + '?_next='+urllib.quote(next) 2528 ) 2529 if not self.has_permission(name, table_name, record_id): 2530 current.session.flash = self.messages.access_denied 2531 return call_or_redirect(self.settings.on_failed_authorization) 2532 return action(*a, **b)
2533 f.__doc__ = action.__doc__ 2534 f.__name__ = action.__name__ 2535 f.__dict__.update(action.__dict__) 2536 return f 2537 2538 return decorator 2539
2540 - def requires_signature(self):
2541 """ 2542 decorator that prevents access to action if not logged in or 2543 if user logged in is not a member of group_id. 2544 If role is provided instead of group_id then the 2545 group_id is calculated. 2546 """ 2547 2548 def decorator(action): 2549 def f(*a, **b): 2550 if self.settings.allow_basic_login_only and not self.basic(): 2551 if current.request.is_restful: 2552 raise HTTP(403,"Not authorized") 2553 return call_or_redirect(self.settings.on_failed_authorization) 2554 2555 if not self.basic() and not self.is_logged_in(): 2556 if current.request.is_restful: 2557 raise HTTP(403,"Not authorized") 2558 request = current.request 2559 next = URL(r=request,args=request.args, 2560 vars=request.get_vars) 2561 current.session.flash = current.response.flash 2562 return call_or_redirect( 2563 self.settings.on_failed_authentication, 2564 self.settings.login_url + '?_next='+urllib.quote(next) 2565 ) 2566 if not URL.verify(current.request,user_signature=True): 2567 current.session.flash = self.messages.access_denied 2568 return call_or_redirect(self.settings.on_failed_authorization) 2569 return action(*a, **b)
2570 f.__doc__ = action.__doc__ 2571 f.__name__ = action.__name__ 2572 f.__dict__.update(action.__dict__) 2573 return f 2574 2575 return decorator 2576
2577 - def add_group(self, role, description=''):
2578 """ 2579 creates a group associated to a role 2580 """ 2581 2582 group_id = self.settings.table_group.insert(role=role, 2583 description=description) 2584 log = self.messages.add_group_log 2585 if log: 2586 self.log_event(log % dict(group_id=group_id, role=role)) 2587 return group_id
2588
2589 - def del_group(self, group_id):
2590 """ 2591 deletes a group 2592 """ 2593 2594 self.db(self.settings.table_group.id == group_id).delete() 2595 self.db(self.settings.table_membership.group_id 2596 == group_id).delete() 2597 self.db(self.settings.table_permission.group_id 2598 == group_id).delete() 2599 log = self.messages.del_group_log 2600 if log: 2601 self.log_event(log % dict(group_id=group_id))
2602
2603 - def id_group(self, role):
2604 """ 2605 returns the group_id of the group specified by the role 2606 """ 2607 rows = self.db(self.settings.table_group.role == role).select() 2608 if not rows: 2609 return None 2610 return rows[0].id
2611
2612 - def user_group(self, user_id = None):
2613 """ 2614 returns the group_id of the group uniquely associated to this user 2615 i.e. role=user:[user_id] 2616 """ 2617 if not user_id and self.user: 2618 user_id = self.user.id 2619 role = 'user_%s' % user_id 2620 return self.id_group(role)
2621
2622 - def has_membership(self, group_id=None, user_id=None, role=None):
2623 """ 2624 checks if user is member of group_id or role 2625 """ 2626 2627 group_id = group_id or self.id_group(role) 2628 try: 2629 group_id = int(group_id) 2630 except: 2631 group_id = self.id_group(group_id) # interpret group_id as a role 2632 if not user_id and self.user: 2633 user_id = self.user.id 2634 membership = self.settings.table_membership 2635 if self.db((membership.user_id == user_id) 2636 & (membership.group_id == group_id)).select(): 2637 r = True 2638 else: 2639 r = False 2640 log = self.messages.has_membership_log 2641 if log: 2642 self.log_event(log % dict(user_id=user_id, 2643 group_id=group_id, check=r)) 2644 return r
2645
2646 - def add_membership(self, group_id=None, user_id=None, role=None):
2647 """ 2648 gives user_id membership of group_id or role 2649 if user_id==None than user_id is that of current logged in user 2650 """ 2651 2652 group_id = group_id or self.id_group(role) 2653 try: 2654 group_id = int(group_id) 2655 except: 2656 group_id = self.id_group(group_id) # interpret group_id as a role 2657 if not user_id and self.user: 2658 user_id = self.user.id 2659 membership = self.settings.table_membership 2660 record = membership(user_id = user_id,group_id = group_id) 2661 if record: 2662 return record.id 2663 else: 2664 id = membership.insert(group_id=group_id, user_id=user_id) 2665 log = self.messages.add_membership_log 2666 if log: 2667 self.log_event(log % dict(user_id=user_id, 2668 group_id=group_id)) 2669 return id
2670
2671 - def del_membership(self, group_id, user_id=None, role=None):
2672 """ 2673 revokes membership from group_id to user_id 2674 if user_id==None than user_id is that of current logged in user 2675 """ 2676 2677 group_id = group_id or self.id_group(role) 2678 if not user_id and self.user: 2679 user_id = self.user.id 2680 membership = self.settings.table_membership 2681 log = self.messages.del_membership_log 2682 if log: 2683 self.log_event(log % dict(user_id=user_id, 2684 group_id=group_id)) 2685 return self.db(membership.user_id 2686 == user_id)(membership.group_id 2687 == group_id).delete()
2688
2689 - def has_permission( 2690 self, 2691 name='any', 2692 table_name='', 2693 record_id=0, 2694 user_id=None, 2695 group_id=None, 2696 ):
2697 """ 2698 checks if user_id or current logged in user is member of a group 2699 that has 'name' permission on 'table_name' and 'record_id' 2700 if group_id is passed, it checks whether the group has the permission 2701 """ 2702 2703 if not user_id and not group_id and self.user: 2704 user_id = self.user.id 2705 if user_id: 2706 membership = self.settings.table_membership 2707 rows = self.db(membership.user_id 2708 == user_id).select(membership.group_id) 2709 groups = set([row.group_id for row in rows]) 2710 if group_id and not group_id in groups: 2711 return False 2712 else: 2713 groups = set([group_id]) 2714 permission = self.settings.table_permission 2715 rows = self.db(permission.name == name)(permission.table_name 2716 == str(table_name))(permission.record_id 2717 == record_id).select(permission.group_id) 2718 groups_required = set([row.group_id for row in rows]) 2719 if record_id: 2720 rows = self.db(permission.name 2721 == name)(permission.table_name 2722 == str(table_name))(permission.record_id 2723 == 0).select(permission.group_id) 2724 groups_required = groups_required.union(set([row.group_id 2725 for row in rows])) 2726 if groups.intersection(groups_required): 2727 r = True 2728 else: 2729 r = False 2730 log = self.messages.has_permission_log 2731 if log and user_id: 2732 self.log_event(log % dict(user_id=user_id, name=name, 2733 table_name=table_name, record_id=record_id)) 2734 return r
2735
2736 - def add_permission( 2737 self, 2738 group_id, 2739 name='any', 2740 table_name='', 2741 record_id=0, 2742 ):
2743 """ 2744 gives group_id 'name' access to 'table_name' and 'record_id' 2745 """ 2746 2747 permission = self.settings.table_permission 2748 if group_id == 0: 2749 group_id = self.user_group() 2750 id = permission.insert(group_id=group_id, name=name, 2751 table_name=str(table_name), 2752 record_id=long(record_id)) 2753 log = self.messages.add_permission_log 2754 if log: 2755 self.log_event(log % dict(permission_id=id, group_id=group_id, 2756 name=name, table_name=table_name, 2757 record_id=record_id)) 2758 return id
2759
2760 - def del_permission( 2761 self, 2762 group_id, 2763 name='any', 2764 table_name='', 2765 record_id=0, 2766 ):
2767 """ 2768 revokes group_id 'name' access to 'table_name' and 'record_id' 2769 """ 2770 2771 permission = self.settings.table_permission 2772 log = self.messages.del_permission_log 2773 if log: 2774 self.log_event(log % dict(group_id=group_id, name=name, 2775 table_name=table_name, record_id=record_id)) 2776 return self.db(permission.group_id == group_id)(permission.name 2777 == name)(permission.table_name 2778 == str(table_name))(permission.record_id 2779 == long(record_id)).delete()
2780
2781 - def accessible_query(self, name, table, user_id=None):
2782 """ 2783 returns a query with all accessible records for user_id or 2784 the current logged in user 2785 this method does not work on GAE because uses JOIN and IN 2786 2787 example:: 2788 2789 db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL) 2790 2791 """ 2792 if not user_id: 2793 user_id = self.user.id 2794 if self.has_permission(name, table, 0, user_id): 2795 return table.id > 0 2796 db = self.db 2797 membership = self.settings.table_membership 2798 permission = self.settings.table_permission 2799 return table.id.belongs(db(membership.user_id == user_id)\ 2800 (membership.group_id == permission.group_id)\ 2801 (permission.name == name)\ 2802 (permission.table_name == table)\ 2803 ._select(permission.record_id))
2804 2805
2806 -class Crud(object):
2807
2808 - def url(self, f=None, args=[], vars={}):
2809 """ 2810 this should point to the controller that exposes 2811 download and crud 2812 """ 2813 return URL(c=self.settings.controller,f=f,args=args,vars=vars)
2814
2815 - def __init__(self, environment, db=None, controller='default'):
2816 self.db = db 2817 if not db and environment and isinstance(environment,DAL): 2818 self.db = environment 2819 elif not db: 2820 raise SyntaxError, "must pass db as first or second argument" 2821 self.environment = current 2822 settings = self.settings = Settings() 2823 settings.auth = None 2824 settings.logger = None 2825 2826 settings.create_next = None 2827 settings.update_next = None 2828 settings.controller = controller 2829 settings.delete_next = self.url() 2830 settings.download_url = self.url('download') 2831 settings.create_onvalidation = StorageList() 2832 settings.update_onvalidation = StorageList() 2833 settings.delete_onvalidation = StorageList() 2834 settings.create_onaccept = StorageList() 2835 settings.update_onaccept = StorageList() 2836 settings.update_ondelete = StorageList() 2837 settings.delete_onaccept = StorageList() 2838 settings.update_deletable = True 2839 settings.showid = False 2840 settings.keepvalues = False 2841 settings.create_captcha = None 2842 settings.update_captcha = None 2843 settings.captcha = None 2844 settings.formstyle = 'table3cols' 2845 settings.hideerror = False 2846 settings.detect_record_change = True 2847 settings.hmac_key = None 2848 settings.lock_keys = True 2849 2850 messages = self.messages = Messages(current.T) 2851 messages.submit_button = 'Submit' 2852 messages.delete_label = 'Check to delete:' 2853 messages.record_created = 'Record Created' 2854 messages.record_updated = 'Record Updated' 2855 messages.record_deleted = 'Record Deleted' 2856 2857 messages.update_log = 'Record %(id)s updated' 2858 messages.create_log = 'Record %(id)s created' 2859 messages.read_log = 'Record %(id)s read' 2860 messages.delete_log = 'Record %(id)s deleted' 2861 2862 messages.lock_keys = True
2863
2864 - def __call__(self):
2865 args = current.request.args 2866 if len(args) < 1: 2867 raise HTTP(404) 2868 elif args[0] == 'tables': 2869 return self.tables() 2870 elif len(args) > 1 and not args(1) in self.db.tables: 2871 raise HTTP(404) 2872 table = self.db[args(1)] 2873 if args[0] == 'create': 2874 return self.create(table) 2875 elif args[0] == 'select': 2876 return self.select(table,linkto=self.url(args='read')) 2877 elif args[0] == 'search': 2878 form, rows = self.search(table,linkto=self.url(args='read')) 2879 return DIV(form,SQLTABLE(rows)) 2880 elif args[0] == 'read': 2881 return self.read(table, args(2)) 2882 elif args[0] == 'update': 2883 return self.update(table, args(2)) 2884 elif args[0] == 'delete': 2885 return self.delete(table, args(2)) 2886 else: 2887 raise HTTP(404)
2888
2889 - def log_event(self, message):
2890 if self.settings.logger: 2891 self.settings.logger.log_event(message, 'crud')
2892
2893 - def has_permission(self, name, table, record=0):
2894 if not self.settings.auth: 2895 return True 2896 try: 2897 record_id = record.id 2898 except: 2899 record_id = record 2900 return self.settings.auth.has_permission(name, str(table), record_id)
2901
2902 - def tables(self):
2903 return TABLE(*[TR(A(name, 2904 _href=self.url(args=('select',name)))) \ 2905 for name in self.db.tables])
2906 2907 2908 @staticmethod
2909 - def archive(form,archive_table=None,current_record='current_record'):
2910 """ 2911 If you have a table (db.mytable) that needs full revision history you can just do:: 2912 2913 form=crud.update(db.mytable,myrecord,onaccept=crud.archive) 2914 2915 crud.archive will define a new table "mytable_archive" and store the 2916 previous record in the newly created table including a reference 2917 to the current record. 2918 2919 If you want to access such table you need to define it yourself in a model:: 2920 2921 db.define_table('mytable_archive', 2922 Field('current_record',db.mytable), 2923 db.mytable) 2924 2925 Notice such table includes all fields of db.mytable plus one: current_record. 2926 crud.archive does not timestamp the stored record unless your original table 2927 has a fields like:: 2928 2929 db.define_table(..., 2930 Field('saved_on','datetime', 2931 default=request.now,update=request.now,writable=False), 2932 Field('saved_by',auth.user, 2933 default=auth.user_id,update=auth.user_id,writable=False), 2934 2935 there is nothing special about these fields since they are filled before 2936 the record is archived. 2937 2938 If you want to change the archive table name and the name of the reference field 2939 you can do, for example:: 2940 2941 db.define_table('myhistory', 2942 Field('parent_record',db.mytable), 2943 db.mytable) 2944 2945 and use it as:: 2946 2947 form=crud.update(db.mytable,myrecord, 2948 onaccept=lambda form:crud.archive(form, 2949 archive_table=db.myhistory, 2950 current_record='parent_record')) 2951 2952 """ 2953 old_record = form.record 2954 if not old_record: 2955 return None 2956 table = form.table 2957 if not archive_table: 2958 archive_table_name = '%s_archive' % table 2959 if archive_table_name in table._db: 2960 archive_table = table._db[archive_table_name] 2961 else: 2962 archive_table = table._db.define_table(archive_table_name, 2963 Field(current_record,table), 2964 table) 2965 new_record = {current_record:old_record.id} 2966 for fieldname in archive_table.fields: 2967 if not fieldname in ['id',current_record] and fieldname in old_record: 2968 new_record[fieldname]=old_record[fieldname] 2969 id = archive_table.insert(**new_record) 2970 return id
2971
2972 - def update( 2973 self, 2974 table, 2975 record, 2976 next=DEFAULT, 2977 onvalidation=DEFAULT, 2978 onaccept=DEFAULT, 2979 ondelete=DEFAULT, 2980 log=DEFAULT, 2981 message=DEFAULT, 2982 deletable=DEFAULT, 2983 formname=DEFAULT, 2984 ):
2985 """ 2986 .. method:: Crud.update(table, record, [next=DEFAULT 2987 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT 2988 [, message=DEFAULT[, deletable=DEFAULT]]]]]]) 2989 2990 """ 2991 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 2992 or (isinstance(record, str) and not str(record).isdigit()): 2993 raise HTTP(404) 2994 if not isinstance(table, self.db.Table): 2995 table = self.db[table] 2996 try: 2997 record_id = record.id 2998 except: 2999 record_id = record or 0 3000 if record_id and not self.has_permission('update', table, record_id): 3001 redirect(self.settings.auth.settings.on_failed_authorization) 3002 if not record_id \ 3003 and not self.has_permission('create', table, record_id): 3004 redirect(self.settings.auth.settings.on_failed_authorization) 3005 3006 request = current.request 3007 response = current.response 3008 session = current.session 3009 if request.extension == 'json' and request.vars.json: 3010 request.vars.update(simplejson.loads(request.vars.json)) 3011 if next == DEFAULT: 3012 next = request.get_vars._next \ 3013 or request.post_vars._next \ 3014 or self.settings.update_next 3015 if onvalidation == DEFAULT: 3016 onvalidation = self.settings.update_onvalidation 3017 if onaccept == DEFAULT: 3018 onaccept = self.settings.update_onaccept 3019 if ondelete == DEFAULT: 3020 ondelete = self.settings.update_ondelete 3021 if log == DEFAULT: 3022 log = self.messages.update_log 3023 if deletable == DEFAULT: 3024 deletable = self.settings.update_deletable 3025 if message == DEFAULT: 3026 message = self.messages.record_updated 3027 form = SQLFORM( 3028 table, 3029 record, 3030 hidden=dict(_next=next), 3031 showid=self.settings.showid, 3032 submit_button=self.messages.submit_button, 3033 delete_label=self.messages.delete_label, 3034 deletable=deletable, 3035 upload=self.settings.download_url, 3036 formstyle=self.settings.formstyle 3037 ) 3038 self.accepted = False 3039 self.deleted = False 3040 captcha = self.settings.update_captcha or \ 3041 self.settings.captcha 3042 if record and captcha: 3043 addrow(form, captcha.label, captcha, captcha.comment, 3044 self.settings.formstyle,'captcha__row') 3045 captcha = self.settings.create_captcha or \ 3046 self.settings.captcha 3047 if not record and captcha: 3048 addrow(form, captcha.label, captcha, captcha.comment, 3049 self.settings.formstyle,'captcha__row') 3050 if not request.extension in ('html','load'): 3051 (_session, _formname) = (None, None) 3052 else: 3053 (_session, _formname) = \ 3054 (session, '%s/%s' % (table._tablename, form.record_id)) 3055 if formname!=DEFAULT: 3056 _formname = formname 3057 keepvalues = self.settings.keepvalues 3058 if request.vars.delete_this_record: 3059 keepvalues = False 3060 if isinstance(onvalidation,StorageList): 3061 onvalidation=onvalidation.get(table._tablename, []) 3062 if form.accepts(request, _session, formname=_formname, 3063 onvalidation=onvalidation, keepvalues=keepvalues, 3064 hideerror=self.settings.hideerror, 3065 detect_record_change = self.settings.detect_record_change): 3066 self.accepted = True 3067 response.flash = message 3068 if log: 3069 self.log_event(log % form.vars) 3070 if request.vars.delete_this_record: 3071 self.deleted = True 3072 message = self.messages.record_deleted 3073 callback(ondelete,form,table._tablename) 3074 response.flash = message 3075 callback(onaccept,form,table._tablename) 3076 if not request.extension in ('html','load'): 3077 raise HTTP(200, 'RECORD CREATED/UPDATED') 3078 if isinstance(next, (list, tuple)): ### fix issue with 2.6 3079 next = next[0] 3080 if next: # Only redirect when explicit 3081 if next[0] != '/' and next[:4] != 'http': 3082 next = URL(r=request, 3083 f=next.replace('[id]', str(form.vars.id))) 3084 session.flash = response.flash 3085 redirect(next) 3086 elif not request.extension in ('html','load'): 3087 raise HTTP(401) 3088 return form
3089
3090 - def create( 3091 self, 3092 table, 3093 next=DEFAULT, 3094 onvalidation=DEFAULT, 3095 onaccept=DEFAULT, 3096 log=DEFAULT, 3097 message=DEFAULT, 3098 formname=DEFAULT, 3099 ):
3100 """ 3101 .. method:: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT 3102 [, onaccept=DEFAULT [, log=DEFAULT[, message=DEFAULT]]]]]) 3103 """ 3104 3105 if next == DEFAULT: 3106 next = self.settings.create_next 3107 if onvalidation == DEFAULT: 3108 onvalidation = self.settings.create_onvalidation 3109 if onaccept == DEFAULT: 3110 onaccept = self.settings.create_onaccept 3111 if log == DEFAULT: 3112 log = self.messages.create_log 3113 if message == DEFAULT: 3114 message = self.messages.record_created 3115 return self.update( 3116 table, 3117 None, 3118 next=next, 3119 onvalidation=onvalidation, 3120 onaccept=onaccept, 3121 log=log, 3122 message=message, 3123 deletable=False, 3124 formname=formname, 3125 )
3126
3127 - def read(self, table, record):
3128 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 3129 or (isinstance(record, str) and not str(record).isdigit()): 3130 raise HTTP(404) 3131 if not isinstance(table, self.db.Table): 3132 table = self.db[table] 3133 if not self.has_permission('read', table, record): 3134 redirect(self.settings.auth.settings.on_failed_authorization) 3135 form = SQLFORM( 3136 table, 3137 record, 3138 readonly=True, 3139 comments=False, 3140 upload=self.settings.download_url, 3141 showid=self.settings.showid, 3142 formstyle=self.settings.formstyle 3143 ) 3144 if not current.request.extension in ('html','load'): 3145 return table._filter_fields(form.record, id=True) 3146 return form
3147
3148 - def delete( 3149 self, 3150 table, 3151 record_id, 3152 next=DEFAULT, 3153 message=DEFAULT, 3154 ):
3155 """ 3156 .. method:: Crud.delete(table, record_id, [next=DEFAULT 3157 [, message=DEFAULT]]) 3158 """ 3159 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 3160 or not str(record_id).isdigit(): 3161 raise HTTP(404) 3162 if not isinstance(table, self.db.Table): 3163 table = self.db[table] 3164 if not self.has_permission('delete', table, record_id): 3165 redirect(self.settings.auth.settings.on_failed_authorization) 3166 request = current.request 3167 session = current.session 3168 if next == DEFAULT: 3169 next = request.get_vars._next \ 3170 or request.post_vars._next \ 3171 or self.settings.delete_next 3172 if message == DEFAULT: 3173 message = self.messages.record_deleted 3174 record = table[record_id] 3175 if record: 3176 callback(self.settings.delete_onvalidation,record) 3177 del table[record_id] 3178 callback(self.settings.delete_onaccept,record,table._tablename) 3179 session.flash = message 3180 if next: # Only redirect when explicit 3181 redirect(next)
3182
3183 - def rows( 3184 self, 3185 table, 3186 query=None, 3187 fields=None, 3188 orderby=None, 3189 limitby=None, 3190 ):
3191 request = current.request 3192 if not (isinstance(table, self.db.Table) or table in self.db.tables): 3193 raise HTTP(404) 3194 if not self.has_permission('select', table): 3195 redirect(self.settings.auth.settings.on_failed_authorization) 3196 #if record_id and not self.has_permission('select', table): 3197 # redirect(self.settings.auth.settings.on_failed_authorization) 3198 if not isinstance(table, self.db.Table): 3199 table = self.db[table] 3200 if not query: 3201 query = table.id > 0 3202 if not fields: 3203 fields = [field for field in table if field.readable] 3204 rows = self.db(query).select(*fields,**dict(orderby=orderby, 3205 limitby=limitby)) 3206 return rows
3207
3208 - def select( 3209 self, 3210 table, 3211 query=None, 3212 fields=None, 3213 orderby=None, 3214 limitby=None, 3215 headers={}, 3216 **attr 3217 ):
3218 rows = self.rows(table,query,fields,orderby,limitby) 3219 if not rows: 3220 return None # Nicer than an empty table. 3221 if not 'upload' in attr: 3222 attr['upload'] = self.url('download') 3223 if not current.request.extension in ('html','load'): 3224 return rows.as_list() 3225 if not headers: 3226 headers = dict((str(k),k.label) for k in table) 3227 return SQLTABLE(rows,headers=headers,**attr)
3228
3229 - def get_format(self, field):
3230 rtable = field._db[field.type[10:]] 3231 format = rtable.get('_format', None) 3232 if format and isinstance(format, str): 3233 return format[2:-2] 3234 return field.name
3235
3236 - def get_query(self, field, op, value, refsearch=False):
3237 try: 3238 if refsearch: format = self.get_format(field) 3239 if op == 'equals': 3240 if not refsearch: 3241 return field == value 3242 else: 3243 return lambda row: row[field.name][format] == value 3244 elif op == 'not equal': 3245 if not refsearch: 3246 return field != value 3247 else: 3248 return lambda row: row[field.name][format] != value 3249 elif op == 'greater than': 3250 if not refsearch: 3251 return field > value 3252 else: 3253 return lambda row: row[field.name][format] > value 3254 elif op == 'less than': 3255 if not refsearch: 3256 return field < value 3257 else: 3258 return lambda row: row[field.name][format] < value 3259 elif op == 'starts with': 3260 if not refsearch: 3261 return field.like(value+'%') 3262 else: 3263 return lambda row: str(row[field.name][format]).startswith(value) 3264 elif op == 'ends with': 3265 if not refsearch: 3266 return field.like('%'+value) 3267 else: 3268 return lambda row: str(row[field.name][format]).endswith(value) 3269 elif op == 'contains': 3270 if not refsearch: 3271 return field.like('%'+value+'%') 3272 else: 3273 return lambda row: value in row[field.name][format] 3274 except: 3275 return None
3276 3277
3278 - def search(self, *tables, **args):
3279 """ 3280 Creates a search form and its results for a table 3281 Example usage: 3282 form, results = crud.search(db.test, 3283 queries = ['equals', 'not equal', 'contains'], 3284 query_labels={'equals':'Equals', 3285 'not equal':'Not equal'}, 3286 fields = [db.test.id, db.test.children], 3287 field_labels = {'id':'ID','children':'Children'}, 3288 zero='Please choose', 3289 query = (db.test.id > 0)&(db.test.id != 3) ) 3290 """ 3291 table = tables[0] 3292 fields = args.get('fields', table.fields) 3293 request = current.request 3294 db = self.db 3295 if not (isinstance(table, db.Table) or table in db.tables): 3296 raise HTTP(404) 3297 attributes = {} 3298 for key in ('orderby','groupby','left','distinct','limitby','cache'): 3299 if key in args: attributes[key]=args[key] 3300 tbl = TABLE() 3301 selected = []; refsearch = []; results = [] 3302 ops = args.get('queries', []) 3303 zero = args.get('zero', '') 3304 if not ops: 3305 ops = ['equals', 'not equal', 'greater than', 3306 'less than', 'starts with', 3307 'ends with', 'contains'] 3308 ops.insert(0,zero) 3309 query_labels = args.get('query_labels', {}) 3310 query = args.get('query',table.id > 0) 3311 field_labels = args.get('field_labels',{}) 3312 for field in fields: 3313 field = table[field] 3314 if not field.readable: continue 3315 fieldname = field.name 3316 chkval = request.vars.get('chk' + fieldname, None) 3317 txtval = request.vars.get('txt' + fieldname, None) 3318 opval = request.vars.get('op' + fieldname, None) 3319 row = TR(TD(INPUT(_type = "checkbox", _name = "chk" + fieldname, 3320 _disabled = (field.type == 'id'), 3321 value = (field.type == 'id' or chkval == 'on'))), 3322 TD(field_labels.get(fieldname,field.label)), 3323 TD(SELECT([OPTION(query_labels.get(op,op), 3324 _value=op) for op in ops], 3325 _name = "op" + fieldname, 3326 value = opval)), 3327 TD(INPUT(_type = "text", _name = "txt" + fieldname, 3328 _value = txtval, _id='txt' + fieldname, 3329 _class = str(field.type)))) 3330 tbl.append(row) 3331 if request.post_vars and (chkval or field.type=='id'): 3332 if txtval and opval != '': 3333 if field.type[0:10] == 'reference ': 3334 refsearch.append(self.get_query(field, 3335 opval, txtval, refsearch=True)) 3336 else: 3337 value, error = field.validate(txtval) 3338 if not error: 3339 ### TODO deal with 'starts with', 'ends with', 'contains' on GAE 3340 query &= self.get_query(field, opval, value) 3341 else: 3342 row[3].append(DIV(error,_class='error')) 3343 selected.append(field) 3344 form = FORM(tbl,INPUT(_type="submit")) 3345 if selected: 3346 try: 3347 results = db(query).select(*selected,**attributes) 3348 for r in refsearch: 3349 results = results.find(r) 3350 except: # hmmm, we should do better here 3351 results = None 3352 return form, results
3353 3354 3355 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor())) 3356
3357 -def fetch(url, data=None, headers={}, 3358 cookie=Cookie.SimpleCookie(), 3359 user_agent='Mozilla/5.0'):
3360 if data != None: 3361 data = urllib.urlencode(data) 3362 if user_agent: headers['User-agent'] = user_agent 3363 headers['Cookie'] = ' '.join(['%s=%s;'%(c.key,c.value) for c in cookie.values()]) 3364 try: 3365 from google.appengine.api import urlfetch 3366 except ImportError: 3367 req = urllib2.Request(url, data, headers) 3368 html = urllib2.urlopen(req).read() 3369 else: 3370 method = ((data==None) and urlfetch.GET) or urlfetch.POST 3371 while url is not None: 3372 response = urlfetch.fetch(url=url, payload=data, 3373 method=method, headers=headers, 3374 allow_truncated=False,follow_redirects=False, 3375 deadline=10) 3376 # next request will be a get, so no need to send the data again 3377 data = None 3378 method = urlfetch.GET 3379 # load cookies from the response 3380 cookie.load(response.headers.get('set-cookie', '')) 3381 url = response.headers.get('location') 3382 html = response.content 3383 return html
3384 3385 regex_geocode = \ 3386 re.compile('\<coordinates\>(?P<la>[^,]*),(?P<lo>[^,]*).*?\</coordinates\>') 3387 3388
3389 -def geocode(address):
3390 try: 3391 a = urllib.quote(address) 3392 txt = fetch('http://maps.google.com/maps/geo?q=%s&output=xml' 3393 % a) 3394 item = regex_geocode.search(txt) 3395 (la, lo) = (float(item.group('la')), float(item.group('lo'))) 3396 return (la, lo) 3397 except: 3398 return (0.0, 0.0)
3399 3400
3401 -def universal_caller(f, *a, **b):
3402 c = f.func_code.co_argcount 3403 n = f.func_code.co_varnames[:c] 3404 b = dict([(k, v) for k, v in b.items() if k in n]) 3405 if len(b) == c: 3406 return f(**b) 3407 elif len(a) >= c: 3408 return f(*a[:c]) 3409 raise HTTP(404, "Object does not exist")
3410 3411
3412 -class Service(object):
3413
3414 - def __init__(self, environment=None):
3415 self.run_procedures = {} 3416 self.csv_procedures = {} 3417 self.xml_procedures = {} 3418 self.rss_procedures = {} 3419 self.json_procedures = {} 3420 self.jsonrpc_procedures = {} 3421 self.xmlrpc_procedures = {} 3422 self.amfrpc_procedures = {} 3423 self.amfrpc3_procedures = {} 3424 self.soap_procedures = {}
3425
3426 - def run(self, f):
3427 """ 3428 example:: 3429 3430 service = Service(globals()) 3431 @service.run 3432 def myfunction(a, b): 3433 return a + b 3434 def call(): 3435 return service() 3436 3437 Then call it with:: 3438 3439 wget http://..../app/default/call/run/myfunction?a=3&b=4 3440 3441 """ 3442 self.run_procedures[f.__name__] = f 3443 return f
3444
3445 - def csv(self, f):
3446 """ 3447 example:: 3448 3449 service = Service(globals()) 3450 @service.csv 3451 def myfunction(a, b): 3452 return a + b 3453 def call(): 3454 return service() 3455 3456 Then call it with:: 3457 3458 wget http://..../app/default/call/csv/myfunction?a=3&b=4 3459 3460 """ 3461 self.run_procedures[f.__name__] = f 3462 return f
3463
3464 - def xml(self, f):
3465 """ 3466 example:: 3467 3468 service = Service(globals()) 3469 @service.xml 3470 def myfunction(a, b): 3471 return a + b 3472 def call(): 3473 return service() 3474 3475 Then call it with:: 3476 3477 wget http://..../app/default/call/xml/myfunction?a=3&b=4 3478 3479 """ 3480 self.run_procedures[f.__name__] = f 3481 return f
3482
3483 - def rss(self, f):
3484 """ 3485 example:: 3486 3487 service = Service(globals()) 3488 @service.rss 3489 def myfunction(): 3490 return dict(title=..., link=..., description=..., 3491 created_on=..., entries=[dict(title=..., link=..., 3492 description=..., created_on=...]) 3493 def call(): 3494 return service() 3495 3496 Then call it with:: 3497 3498 wget http://..../app/default/call/rss/myfunction 3499 3500 """ 3501 self.rss_procedures[f.__name__] = f 3502 return f
3503
3504 - def json(self, f):
3505 """ 3506 example:: 3507 3508 service = Service(globals()) 3509 @service.json 3510 def myfunction(a, b): 3511 return [{a: b}] 3512 def call(): 3513 return service() 3514 3515 Then call it with:: 3516 3517 wget http://..../app/default/call/json/myfunction?a=hello&b=world 3518 3519 """ 3520 self.json_procedures[f.__name__] = f 3521 return f
3522
3523 - def jsonrpc(self, f):
3524 """ 3525 example:: 3526 3527 service = Service(globals()) 3528 @service.jsonrpc 3529 def myfunction(a, b): 3530 return a + b 3531 def call(): 3532 return service() 3533 3534 Then call it with:: 3535 3536 wget http://..../app/default/call/jsonrpc/myfunction?a=hello&b=world 3537 3538 """ 3539 self.jsonrpc_procedures[f.__name__] = f 3540 return f
3541
3542 - def xmlrpc(self, f):
3543 """ 3544 example:: 3545 3546 service = Service(globals()) 3547 @service.xmlrpc 3548 def myfunction(a, b): 3549 return a + b 3550 def call(): 3551 return service() 3552 3553 The call it with:: 3554 3555 wget http://..../app/default/call/xmlrpc/myfunction?a=hello&b=world 3556 3557 """ 3558 self.xmlrpc_procedures[f.__name__] = f 3559 return f
3560
3561 - def amfrpc(self, f):
3562 """ 3563 example:: 3564 3565 service = Service(globals()) 3566 @service.amfrpc 3567 def myfunction(a, b): 3568 return a + b 3569 def call(): 3570 return service() 3571 3572 The call it with:: 3573 3574 wget http://..../app/default/call/amfrpc/myfunction?a=hello&b=world 3575 3576 """ 3577 self.amfrpc_procedures[f.__name__] = f 3578 return f
3579
3580 - def amfrpc3(self, domain='default'):
3581 """ 3582 example:: 3583 3584 service = Service(globals()) 3585 @service.amfrpc3('domain') 3586 def myfunction(a, b): 3587 return a + b 3588 def call(): 3589 return service() 3590 3591 The call it with:: 3592 3593 wget http://..../app/default/call/amfrpc3/myfunction?a=hello&b=world 3594 3595 """ 3596 if not isinstance(domain, str): 3597 raise SyntaxError, "AMF3 requires a domain for function" 3598 3599 def _amfrpc3(f): 3600 if domain: 3601 self.amfrpc3_procedures[domain+'.'+f.__name__] = f 3602 else: 3603 self.amfrpc3_procedures[f.__name__] = f 3604 return f
3605 return _amfrpc3
3606
3607 - def soap(self, name=None, returns=None, args=None,doc=None):
3608 """ 3609 example:: 3610 3611 service = Service(globals()) 3612 @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,}) 3613 def myfunction(a, b): 3614 return a + b 3615 def call(): 3616 return service() 3617 3618 The call it with:: 3619 3620 from gluon.contrib.pysimplesoap.client import SoapClient 3621 client = SoapClient(wsdl="http://..../app/default/call/soap?WSDL") 3622 response = client.MyFunction(a=1,b=2) 3623 return response['result'] 3624 3625 Exposes online generated documentation and xml example messages at: 3626 - http://..../app/default/call/soap 3627 """ 3628 3629 def _soap(f): 3630 self.soap_procedures[name or f.__name__] = f, returns, args, doc 3631 return f
3632 return _soap 3633
3634 - def serve_run(self, args=None):
3635 request = current.request 3636 if not args: 3637 args = request.args 3638 if args and args[0] in self.run_procedures: 3639 return str(universal_caller(self.run_procedures[args[0]], 3640 *args[1:], **dict(request.vars))) 3641 self.error()
3642
3643 - def serve_csv(self, args=None):
3644 request = current.request 3645 response = current.response 3646 response.headers['Content-Type'] = 'text/x-csv' 3647 if not args: 3648 args = request.args 3649 3650 def none_exception(value): 3651 if isinstance(value, unicode): 3652 return value.encode('utf8') 3653 if hasattr(value, 'isoformat'): 3654 return value.isoformat()[:19].replace('T', ' ') 3655 if value == None: 3656 return '<NULL>' 3657 return value
3658 if args and args[0] in self.run_procedures: 3659 r = universal_caller(self.run_procedures[args[0]], 3660 *args[1:], **dict(request.vars)) 3661 s = cStringIO.StringIO() 3662 if hasattr(r, 'export_to_csv_file'): 3663 r.export_to_csv_file(s) 3664 elif r and isinstance(r[0], (dict, Storage)): 3665 import csv 3666 writer = csv.writer(s) 3667 writer.writerow(r[0].keys()) 3668 for line in r: 3669 writer.writerow([none_exception(v) \ 3670 for v in line.values()]) 3671 else: 3672 import csv 3673 writer = csv.writer(s) 3674 for line in r: 3675 writer.writerow(line) 3676 return s.getvalue() 3677 self.error() 3678
3679 - def serve_xml(self, args=None):
3680 request = current.request 3681 response = current.response 3682 response.headers['Content-Type'] = 'text/xml' 3683 if not args: 3684 args = request.args 3685 if args and args[0] in self.run_procedures: 3686 s = universal_caller(self.run_procedures[args[0]], 3687 *args[1:], **dict(request.vars)) 3688 if hasattr(s, 'as_list'): 3689 s = s.as_list() 3690 return serializers.xml(s) 3691 self.error()
3692
3693 - def serve_rss(self, args=None):
3694 request = current.request 3695 response = current.response 3696 if not args: 3697 args = request.args 3698 if args and args[0] in self.rss_procedures: 3699 feed = universal_caller(self.rss_procedures[args[0]], 3700 *args[1:], **dict(request.vars)) 3701 else: 3702 self.error() 3703 response.headers['Content-Type'] = 'application/rss+xml' 3704 return serializers.rss(feed)
3705
3706 - def serve_json(self, args=None):
3707 request = current.request 3708 response = current.response 3709 response.headers['Content-Type'] = 'text/x-json' 3710 if not args: 3711 args = request.args 3712 d = dict(request.vars) 3713 if args and args[0] in self.json_procedures: 3714 s = universal_caller(self.json_procedures[args[0]],*args[1:],**d) 3715 if hasattr(s, 'as_list'): 3716 s = s.as_list() 3717 return response.json(s) 3718 self.error()
3719
3720 - class JsonRpcException(Exception):
3721 - def __init__(self,code,info):
3722 self.code,self.info = code,info
3723
3724 - def serve_jsonrpc(self):
3725 import contrib.simplejson as simplejson 3726 def return_response(id, result): 3727 return serializers.json({'version': '1.1', 3728 'id': id, 'result': result, 'error': None})
3729 3730 def return_error(id, code, message): 3731 return serializers.json({'id': id, 3732 'version': '1.1', 3733 'error': {'name': 'JSONRPCError', 3734 'code': code, 'message': message} 3735 }) 3736 3737 request = current.request 3738 methods = self.jsonrpc_procedures 3739 data = simplejson.loads(request.body.read()) 3740 id, method, params = data['id'], data['method'], data.get('params','') 3741 if not method in methods: 3742 return return_error(id, 100, 'method "%s" does not exist' % method) 3743 try: 3744 s = methods[method](*params) 3745 if hasattr(s, 'as_list'): 3746 s = s.as_list() 3747 return return_response(id, s) 3748 except Service.JsonRpcException, e: 3749 return return_error(id, e.code, e.info) 3750 except BaseException: 3751 etype, eval, etb = sys.exc_info() 3752 return return_error(id, 100, '%s: %s' % (etype.__name__, eval)) 3753 except: 3754 etype, eval, etb = sys.exc_info() 3755 return return_error(id, 100, 'Exception %s: %s' % (etype, eval)) 3756
3757 - def serve_xmlrpc(self):
3758 request = current.request 3759 response = current.response 3760 services = self.xmlrpc_procedures.values() 3761 return response.xmlrpc(request, services)
3762
3763 - def serve_amfrpc(self, version=0):
3764 try: 3765 import pyamf 3766 import pyamf.remoting.gateway 3767 except: 3768 return "pyamf not installed or not in Python sys.path" 3769 request = current.request 3770 response = current.response 3771 if version == 3: 3772 services = self.amfrpc3_procedures 3773 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 3774 pyamf_request = pyamf.remoting.decode(request.body) 3775 else: 3776 services = self.amfrpc_procedures 3777 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 3778 context = pyamf.get_context(pyamf.AMF0) 3779 pyamf_request = pyamf.remoting.decode(request.body, context) 3780 pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion, 3781 pyamf_request.clientType) 3782 for name, message in pyamf_request: 3783 pyamf_response[name] = base_gateway.getProcessor(message)(message) 3784 response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE 3785 if version==3: 3786 return pyamf.remoting.encode(pyamf_response).getvalue() 3787 else: 3788 return pyamf.remoting.encode(pyamf_response, context).getvalue()
3789
3790 - def serve_soap(self, version="1.1"):
3791 try: 3792 from contrib.pysimplesoap.server import SoapDispatcher 3793 except: 3794 return "pysimplesoap not installed in contrib" 3795 request = current.request 3796 response = current.response 3797 procedures = self.soap_procedures 3798 3799 location = "%s://%s%s" % ( 3800 request.env.wsgi_url_scheme, 3801 request.env.http_host, 3802 URL(r=request,f="call/soap",vars={})) 3803 namespace = 'namespace' in response and response.namespace or location 3804 documentation = response.description or '' 3805 dispatcher = SoapDispatcher( 3806 name = response.title, 3807 location = location, 3808 action = location, # SOAPAction 3809 namespace = namespace, 3810 prefix='pys', 3811 documentation = documentation, 3812 ns = True) 3813 for method, (function, returns, args, doc) in procedures.items(): 3814 dispatcher.register_function(method, function, returns, args, doc) 3815 if request.env.request_method == 'POST': 3816 # Process normal Soap Operation 3817 response.headers['Content-Type'] = 'text/xml' 3818 return dispatcher.dispatch(request.body.read()) 3819 elif 'WSDL' in request.vars: 3820 # Return Web Service Description 3821 response.headers['Content-Type'] = 'text/xml' 3822 return dispatcher.wsdl() 3823 elif 'op' in request.vars: 3824 # Return method help webpage 3825 response.headers['Content-Type'] = 'text/html' 3826 method = request.vars['op'] 3827 sample_req_xml, sample_res_xml, doc = dispatcher.help(method) 3828 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 3829 A("See all webservice operations", 3830 _href=URL(r=request,f="call/soap",vars={})), 3831 H2(method), 3832 P(doc), 3833 UL(LI("Location: %s" % dispatcher.location), 3834 LI("Namespace: %s" % dispatcher.namespace), 3835 LI("SoapAction: %s" % dispatcher.action), 3836 ), 3837 H3("Sample SOAP XML Request Message:"), 3838 CODE(sample_req_xml,language="xml"), 3839 H3("Sample SOAP XML Response Message:"), 3840 CODE(sample_res_xml,language="xml"), 3841 ] 3842 return {'body': body} 3843 else: 3844 # Return general help and method list webpage 3845 response.headers['Content-Type'] = 'text/html' 3846 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 3847 P(response.description), 3848 P("The following operations are available"), 3849 A("See WSDL for webservice description", 3850 _href=URL(r=request,f="call/soap",vars={"WSDL":None})), 3851 UL([LI(A("%s: %s" % (method, doc or ''), 3852 _href=URL(r=request,f="call/soap",vars={'op': method}))) 3853 for method, doc in dispatcher.list_methods()]), 3854 ] 3855 return {'body': body}
3856
3857 - def __call__(self):
3858 """ 3859 register services with: 3860 service = Service(globals()) 3861 @service.run 3862 @service.rss 3863 @service.json 3864 @service.jsonrpc 3865 @service.xmlrpc 3866 @service.jsonrpc 3867 @service.amfrpc 3868 @service.amfrpc3('domain') 3869 @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,}) 3870 3871 expose services with 3872 3873 def call(): return service() 3874 3875 call services with 3876 http://..../app/default/call/run?[parameters] 3877 http://..../app/default/call/rss?[parameters] 3878 http://..../app/default/call/json?[parameters] 3879 http://..../app/default/call/jsonrpc 3880 http://..../app/default/call/xmlrpc 3881 http://..../app/default/call/amfrpc 3882 http://..../app/default/call/amfrpc3 3883 http://..../app/default/call/soap 3884 """ 3885 3886 request = current.request 3887 if len(request.args) < 1: 3888 raise HTTP(404, "Not Found") 3889 arg0 = request.args(0) 3890 if arg0 == 'run': 3891 return self.serve_run(request.args[1:]) 3892 elif arg0 == 'rss': 3893 return self.serve_rss(request.args[1:]) 3894 elif arg0 == 'csv': 3895 return self.serve_csv(request.args[1:]) 3896 elif arg0 == 'xml': 3897 return self.serve_xml(request.args[1:]) 3898 elif arg0 == 'json': 3899 return self.serve_json(request.args[1:]) 3900 elif arg0 == 'jsonrpc': 3901 return self.serve_jsonrpc() 3902 elif arg0 == 'xmlrpc': 3903 return self.serve_xmlrpc() 3904 elif arg0 == 'amfrpc': 3905 return self.serve_amfrpc() 3906 elif arg0 == 'amfrpc3': 3907 return self.serve_amfrpc(3) 3908 elif arg0 == 'soap': 3909 return self.serve_soap() 3910 else: 3911 self.error()
3912
3913 - def error(self):
3914 raise HTTP(404, "Object does not exist")
3915 3916
3917 -def completion(callback):
3918 """ 3919 Executes a task on completion of the called action. For example: 3920 3921 from gluon.tools import completion 3922 @completion(lambda d: logging.info(repr(d))) 3923 def index(): 3924 return dict(message='hello') 3925 3926 It logs the output of the function every time input is called. 3927 The argument of completion is executed in a new thread. 3928 """ 3929 def _completion(f): 3930 def __completion(*a,**b): 3931 d = None 3932 try: 3933 d = f(*a,**b) 3934 return d 3935 finally: 3936 thread.start_new_thread(callback,(d,))
3937 return __completion 3938 return _completion 3939
3940 -def prettydate(d,T=lambda x:x):
3941 try: 3942 dt = datetime.datetime.now() - d 3943 except: 3944 return '' 3945 if dt.days >= 2*365: 3946 return T('%d years ago') % int(dt.days / 365) 3947 elif dt.days >= 365: 3948 return T('1 year ago') 3949 elif dt.days >= 60: 3950 return T('%d months ago') % int(dt.days / 30) 3951 elif dt.days > 21: 3952 return T('1 month ago') 3953 elif dt.days >= 14: 3954 return T('%d weeks ago') % int(dt.days / 7) 3955 elif dt.days >= 7: 3956 return T('1 week ago') 3957 elif dt.days > 1: 3958 return T('%d days ago') % dt.days 3959 elif dt.days == 1: 3960 return T('1 day ago') 3961 elif dt.seconds >= 2*60*60: 3962 return T('%d hours ago') % int(dt.seconds / 3600) 3963 elif dt.seconds >= 60*60: 3964 return T('1 hour ago') 3965 elif dt.seconds >= 2*60: 3966 return T('%d minutes ago') % int(dt.seconds / 60) 3967 elif dt.seconds >= 60: 3968 return T('1 minute ago') 3969 elif dt.seconds > 1: 3970 return T('%d seconds ago') % dt.seconds 3971 elif dt.seconds == 1: 3972 return T('1 second ago') 3973 else: 3974 return T('now')
3975
3976 -def test_thread_separation():
3977 def f(): 3978 c=PluginManager() 3979 lock1.acquire() 3980 lock2.acquire() 3981 c.x=7 3982 lock1.release() 3983 lock2.release()
3984 lock1=thread.allocate_lock() 3985 lock2=thread.allocate_lock() 3986 lock1.acquire() 3987 thread.start_new_thread(f,()) 3988 a=PluginManager() 3989 a.x=5 3990 lock1.release() 3991 lock2.acquire() 3992 return a.x 3993
3994 -class PluginManager(object):
3995 """ 3996 3997 Plugin Manager is similar to a storage object but it is a single level singleton 3998 this means that multiple instances within the same thread share the same attributes 3999 Its constructor is also special. The first argument is the name of the plugin you are defining. 4000 The named arguments are parameters needed by the plugin with default values. 4001 If the parameters were previous defined, the old values are used. 4002 4003 For example: 4004 4005 ### in some general configuration file: 4006 >>> plugins = PluginManager() 4007 >>> plugins.me.param1=3 4008 4009 ### within the plugin model 4010 >>> _ = PluginManager('me',param1=5,param2=6,param3=7) 4011 4012 ### where the plugin is used 4013 >>> print plugins.me.param1 4014 3 4015 >>> print plugins.me.param2 4016 6 4017 >>> plugins.me.param3 = 8 4018 >>> print plugins.me.param3 4019 8 4020 4021 Here are some tests: 4022 4023 >>> a=PluginManager() 4024 >>> a.x=6 4025 >>> b=PluginManager('check') 4026 >>> print b.x 4027 6 4028 >>> b=PluginManager() # reset settings 4029 >>> print b.x 4030 <Storage {}> 4031 >>> b.x=7 4032 >>> print a.x 4033 7 4034 >>> a.y.z=8 4035 >>> print b.y.z 4036 8 4037 >>> test_thread_separation() 4038 5 4039 >>> plugins=PluginManager('me',db='mydb') 4040 >>> print plugins.me.db 4041 mydb 4042 >>> print 'me' in plugins 4043 True 4044 >>> print plugins.me.installed 4045 True 4046 """ 4047 instances = {}
4048 - def __new__(cls,*a,**b):
4049 id = thread.get_ident() 4050 lock = thread.allocate_lock() 4051 try: 4052 lock.acquire() 4053 try: 4054 return cls.instances[id] 4055 except KeyError: 4056 instance = object.__new__(cls,*a,**b) 4057 cls.instances[id] = instance 4058 return instance 4059 finally: 4060 lock.release()
4061 - def __init__(self,plugin=None,**defaults):
4062 if not plugin: 4063 self.__dict__.clear() 4064 settings = self.__getattr__(plugin) 4065 settings.installed = True 4066 [settings.update({key:value}) for key,value in defaults.items() if not key in settings]
4067 - def __getattr__(self, key):
4068 if not key in self.__dict__: 4069 self.__dict__[key] = Storage() 4070 return self.__dict__[key]
4071 - def keys(self):
4072 return self.__dict__.keys()
4073 - def __contains__(self,key):
4074 return key in self.__dict__
4075 4076 if __name__ == '__main__': 4077 import doctest 4078 doctest.testmod() 4079