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