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

Source Code for Module web2py.gluon.tools

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