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