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

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