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

# Source Code for Module web2py.gluon.tools

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