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

Source Code for Module web2py.gluon.tools

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