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

Source Code for Module gluon.tools

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