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