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