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