Package web2py :: Package gluon :: Module tools

[hide private]
[frames] | no frames]

# Source Code for Module web2py.gluon.tools

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