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