diff --git a/sphinx/web/__init__.py b/sphinx/web/__init__.py deleted file mode 100644 index 66f0f22bb..000000000 --- a/sphinx/web/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web - ~~~~~~~~~~ - - A web application to serve the Python docs interactively. - - :copyright: 2007-2008 by Georg Brandl. - :license: BSD. -""" - -import os -import sys -import getopt - -import sphinx -from sphinx.web.application import setup_app -from sphinx.web.serve import run_simple - -try: - from werkzeug.debug import DebuggedApplication -except ImportError: - DebuggedApplication = lambda x, y: x - - -def main(argv=sys.argv): - opts, args = getopt.getopt(argv[1:], "dhf:") - opts = dict(opts) - if len(args) != 1 or '-h' in opts: - print 'usage: %s [-d] [-f cfg.py] ' % argv[0] - print ' -d: debug mode, use werkzeug debugger if installed' - print ' -f: use "cfg.py" file instead of doc_root/webconf.py' - return 2 - - conffile = opts.get('-f', os.path.join(args[0], 'webconf.py')) - config = {} - execfile(conffile, config) - - port = config.get('listen_port', 3000) - hostname = config.get('listen_addr', 'localhost') - debug = ('-d' in opts) or (hostname == 'localhost') - - config['data_root_path'] = args[0] - config['debug'] = debug - - def make_app(): - app = setup_app(config, check_superuser=True) - if debug: - app = DebuggedApplication(app, True) - return app - - if os.environ.get('RUN_MAIN') != 'true': - print '* Sphinx %s- Python documentation web application' % \ - sphinx.__version__.replace('$', '').replace('Revision:', 'rev.') - if debug: - print '* Running in debug mode' - - run_simple(hostname, port, make_app, use_reloader=debug) - - -if __name__ == '__main__': - sys.exit(main(sys.argv)) diff --git a/sphinx/web/admin.py b/sphinx/web/admin.py deleted file mode 100644 index 12004475b..000000000 --- a/sphinx/web/admin.py +++ /dev/null @@ -1,258 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.admin - ~~~~~~~~~~~~~~~~ - - Admin application parts. - - :copyright: 2007-2008 by Georg Brandl, Armin Ronacher. - :license: BSD. -""" - -from sphinx.web.util import render_template -from sphinx.web.wsgiutil import Response, RedirectResponse, NotFound -from sphinx.web.database import Comment - - -class AdminPanel(object): - """ - Provide the admin functionallity. - """ - - def __init__(self, app): - self.app = app - self.env = app.env - self.userdb = app.userdb - - def dispatch(self, req, page): - """ - Dispatch the requests for the current user in the admin panel. - """ - is_logged_in = req.user is not None - if is_logged_in: - privileges = self.userdb.privileges[req.user] - is_master_admin = 'master' in privileges - can_change_password = 'frozenpassword' not in privileges - else: - privileges = set() - can_change_password = is_master_admin = False - - # login and logout - if page == 'login': - return self.do_login(req) - elif not is_logged_in: - return RedirectResponse('@admin/login/') - elif page == 'logout': - return self.do_logout(req) - - # account maintance - elif page == 'change_password' and can_change_password: - return self.do_change_password(req) - elif page == 'manage_users' and is_master_admin: - return self.do_manage_users(req) - - # moderate comments - elif page.split('/')[0] == 'moderate_comments': - return self.do_moderate_comments(req, page[18:]) - - # missing page - elif page != '': - raise NotFound() - return Response(render_template(req, 'admin/index.html', { - 'is_master_admin': is_master_admin, - 'can_change_password': can_change_password - })) - - def do_login(self, req): - """ - Display login form and do the login procedure. - """ - if req.user is not None: - return RedirectResponse('@admin/') - login_failed = False - if req.method == 'POST': - if req.form.get('cancel'): - return RedirectResponse('') - username = req.form.get('username') - password = req.form.get('password') - if self.userdb.check_password(username, password): - req.login(username) - return RedirectResponse('@admin/') - login_failed = True - return Response(render_template(req, 'admin/login.html', { - 'login_failed': login_failed - })) - - def do_logout(self, req): - """ - Log the user out. - """ - req.logout() - return RedirectResponse('@admin/login/') - - def do_change_password(self, req): - """ - Allows the user to change his password. - """ - change_failed = change_successful = False - if req.method == 'POST': - if req.form.get('cancel'): - return RedirectResponse('@admin/') - pw = req.form.get('pw1') - if pw and pw == req.form.get('pw2'): - self.userdb.set_password(req.user, pw) - self.userdb.save() - change_successful = True - else: - change_failed = True - return Response(render_template(req, 'admin/change_password.html', { - 'change_failed': change_failed, - 'change_successful': change_successful - })) - - def do_manage_users(self, req): - """ - Manage other user accounts. Requires master privileges. - """ - add_user_mode = False - user_privileges = {} - users = sorted((user, []) for user in self.userdb.users) - to_delete = set() - generated_user = generated_password = None - user_exists = False - - if req.method == 'POST': - for item in req.form.getlist('delete'): - try: - to_delete.add(item) - except ValueError: - pass - for name, item in req.form.iteritems(): - if name.startswith('privileges-'): - user_privileges[name[11:]] = [x.strip() for x - in item.split(',')] - if req.form.get('cancel'): - return RedirectResponse('@admin/') - elif req.form.get('add_user'): - username = req.form.get('username') - if username: - if username in self.userdb.users: - user_exists = username - else: - generated_password = self.userdb.add_user(username) - self.userdb.save() - generated_user = username - else: - add_user_mode = True - elif req.form.get('aborted'): - return RedirectResponse('@admin/manage_users/') - - users = {} - for user in self.userdb.users: - if user not in user_privileges: - users[user] = sorted(self.userdb.privileges[user]) - else: - users[user] = user_privileges[user] - - new_users = users.copy() - for user in to_delete: - new_users.pop(user, None) - - self_destruction = req.user not in new_users or \ - 'master' not in new_users[req.user] - - if req.method == 'POST' and (not to_delete or - (to_delete and req.form.get('confirmed'))) and \ - req.form.get('update'): - old_users = self.userdb.users.copy() - for user in old_users: - if user not in new_users: - del self.userdb.users[user] - else: - self.userdb.privileges[user].clear() - self.userdb.privileges[user].update(new_users[user]) - self.userdb.save() - return RedirectResponse('@admin/manage_users/') - - return Response(render_template(req, 'admin/manage_users.html', { - 'users': users, - 'add_user_mode': add_user_mode, - 'to_delete': to_delete, - 'ask_confirmation': req.method == 'POST' and to_delete \ - and not self_destruction, - 'generated_user': generated_user, - 'generated_password': generated_password, - 'self_destruction': self_destruction, - 'user_exists': user_exists - })) - - def do_moderate_comments(self, req, url): - """ - Comment moderation panel. - """ - if url == 'recent_comments': - details_for = None - recent_comments = Comment.get_recent(20) - else: - details_for = url and self.env.get_real_filename(url) or None - recent_comments = None - to_delete = set() - edit_detail = None - - if 'edit' in req.args: - try: - edit_detail = Comment.get(int(req.args['edit'])) - except ValueError: - pass - - if req.method == 'POST': - for item in req.form.getlist('delete'): - try: - to_delete.add(int(item)) - except ValueError: - pass - if req.form.get('cancel'): - return RedirectResponse('@admin/') - elif req.form.get('confirmed'): - for comment_id in to_delete: - try: - Comment.get(comment_id).delete() - except ValueError: - pass - return RedirectResponse(req.path) - elif req.form.get('aborted'): - return RedirectResponse(req.path) - elif req.form.get('edit') and not to_delete: - if 'delete_this' in req.form: - try: - to_delete.add(req.form['delete_this']) - except ValueError: - pass - else: - try: - edit_detail = c = Comment.get(int(req.args['edit'])) - except ValueError: - pass - else: - if req.form.get('view'): - return RedirectResponse(c.url) - c.author = req.form.get('author', '') - c.author_mail = req.form.get('author_mail', '') - c.title = req.form.get('title', '') - c.comment_body = req.form.get('comment_body', '') - c.save() - self.app.cache.pop(edit_detail.associated_page, None) - return RedirectResponse(req.path) - - return Response(render_template(req, 'admin/moderate_comments.html', { - 'pages_with_comments': [{ - 'page_id': page_id, - 'title': page_id, #XXX: get title somehow - 'has_details': details_for == page_id, - 'comments': comments - } for page_id, comments in Comment.get_overview(details_for)], - 'recent_comments': recent_comments, - 'to_delete': to_delete, - 'ask_confirmation': req.method == 'POST' and to_delete, - 'edit_detail': edit_detail - })) diff --git a/sphinx/web/antispam.py b/sphinx/web/antispam.py deleted file mode 100644 index 3c14d9e41..000000000 --- a/sphinx/web/antispam.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.antispam - ~~~~~~~~~~~~~~~~~~~ - - Small module that performs anti spam tests based on the bad content - regex list provided by moin moin. - - :copyright: 2007-2008 by Armin Ronacher. - :license: BSD. -""" - -import re -import urllib -import time -from os import path - -DOWNLOAD_URL = 'http://moinmaster.wikiwikiweb.de/BadContent?action=raw' -UPDATE_INTERVAL = 60 * 60 * 24 * 7 - - -class AntiSpam(object): - """ - Class that reads a bad content database (flat file that is automatically - updated from the moin moin server) and checks strings against it. - """ - - def __init__(self, bad_content_file): - self.bad_content_file = bad_content_file - lines = None - - if not path.exists(self.bad_content_file): - last_change = 0 - else: - last_change = path.getmtime(self.bad_content_file) - - if last_change + UPDATE_INTERVAL < time.time(): - try: - f = urllib.urlopen(DOWNLOAD_URL) - data = f.read() - except: - pass - else: - lines = [l.strip() for l in data.splitlines() - if not l.startswith('#')] - f = open(bad_content_file, 'w') - try: - f.write('\n'.join(lines)) - finally: - f.close() - last_change = int(time.time()) - - if lines is None: - try: - f = open(bad_content_file) - try: - lines = [l.strip() for l in f] - finally: - f.close() - except: - lines = [] - self.rules = [re.compile(rule) for rule in lines if rule] - - def is_spam(self, fields): - for regex in self.rules: - for field in fields: - if regex.search(field) is not None: - return True - return False diff --git a/sphinx/web/application.py b/sphinx/web/application.py deleted file mode 100644 index a5836f3d7..000000000 --- a/sphinx/web/application.py +++ /dev/null @@ -1,826 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.application - ~~~~~~~~~~~~~~~~~~~~~~ - - A simple WSGI application that serves an interactive version - of the python documentation. - - :copyright: 2007-2008 by Georg Brandl, Armin Ronacher. - :license: BSD. -""" - -import os -import re -import copy -import time -import heapq -import math -import difflib -import tempfile -import threading -import cPickle as pickle -import cStringIO as StringIO -from os import path -from itertools import groupby - -from sphinx.web.feed import Feed -from sphinx.web.mail import Email -from sphinx.web.util import render_template, get_target_uri, blackhole_dict, striptags -from sphinx.web.admin import AdminPanel -from sphinx.web.userdb import UserDatabase -from sphinx.web.robots import robots_txt -from sphinx.web.oldurls import handle_html_url -from sphinx.web.antispam import AntiSpam -from sphinx.web.database import connect, set_connection, Comment -from sphinx.web.wsgiutil import Request, Response, RedirectResponse, \ - JSONResponse, SharedDataMiddleware, NotFound, get_base_uri - -from sphinx.util import relative_uri -from sphinx.search import SearchFrontend -from sphinx.htmlwriter import HTMLWriter -from sphinx.builder import LAST_BUILD_FILENAME, ENV_PICKLE_FILENAME - -from docutils.io import StringOutput -from docutils.utils import Reporter -from docutils.frontend import OptionParser - -_mail_re = re.compile(r'^([a-zA-Z0-9_\.\-])+\@' - r'(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,})+$') - -env_lock = threading.Lock() - - -PATCH_MESSAGE = '''\ -A new documentation patch has been submitted. - Author: %(author)s <%(email)s> - Date: %(asctime)s - Page: %(page_id)s - Summary: %(summary)s - -''' - -known_designs = { - 'default': (['default.css', 'pygments.css'], - 'The default design, with the sidebar on the left side.'), - 'rightsidebar': (['default.css', 'rightsidebar.css', 'pygments.css'], - 'Display the sidebar on the right side.'), - 'stickysidebar': (['default.css', 'stickysidebar.css', 'pygments.css'], - '''\ - Display the sidebar on the left and don\'t scroll it - with the content. This can cause parts of the content to - become inaccessible when the table of contents is too long.'''), - 'traditional': (['traditional.css'], - '''\ - A design similar to the old documentation style.'''), -} - -comments_methods = { - 'inline': 'Show all comments inline.', - 'bottom': 'Show all comments at the page bottom.', - 'none': 'Don\'t show comments at all.', -} - - -class MockBuilder(object): - def get_relative_uri(self, from_, to): - return '' - name = 'web' - - -NoCache = object() - -def cached(inner): - """ - Response caching system. - """ - def caching_function(self, *args, **kwds): - gen = inner(self, *args, **kwds) - cache_id = gen.next() - if cache_id is NoCache: - response = gen.next() - gen.close() - # this could also return a RedirectResponse... - if isinstance(response, Response): - return response - else: - return Response(response) - try: - text = self.cache[cache_id] - gen.close() - except KeyError: - text = gen.next() - self.cache[cache_id] = text - return Response(text) - return caching_function - - -class DocumentationApplication(object): - """ - Serves the documentation. - """ - - def __init__(self, config): - if config['debug']: - self.cache = blackhole_dict() - else: - self.cache = {} - self.freqmodules = {} - self.last_most_frequent = [] - self.generated_stylesheets = {} - self.config = config - self.data_root = config['data_root_path'] - self.buildfile = path.join(self.data_root, LAST_BUILD_FILENAME) - self.buildmtime = -1 - self.load_env(0) - self.db_con = connect(path.join(self.data_root, 'sphinx.db')) - self.antispam = AntiSpam(path.join(self.data_root, 'bad_content')) - self.userdb = UserDatabase(path.join(self.data_root, 'docusers')) - self.admin_panel = AdminPanel(self) - - - def load_env(self, new_mtime): - env_lock.acquire() - try: - if self.buildmtime == new_mtime: - # happens if another thread already reloaded the env - return - print "* Loading the environment..." - f = open(path.join(self.data_root, ENV_PICKLE_FILENAME), 'rb') - try: - self.env = pickle.load(f) - finally: - f.close() - f = open(path.join(self.data_root, 'globalcontext.pickle'), 'rb') - try: - self.globalcontext = pickle.load(f) - finally: - f.close() - f = open(path.join(self.data_root, 'searchindex.pickle'), 'rb') - try: - self.search_frontend = SearchFrontend(pickle.load(f)) - finally: - f.close() - self.buildmtime = new_mtime - self.cache.clear() - finally: - env_lock.release() - - - def search(self, req): - """ - Search the database. Currently just a keyword based search. - """ - if not req.args.get('q'): - return RedirectResponse('') - return RedirectResponse('q/%s/' % req.args['q']) - - - def get_page_source(self, page): - """ - Get the reST source of a page. - """ - page_id = self.env.get_real_filename(page)[:-4] - if page_id is None: - raise NotFound() - filename = path.join(self.data_root, 'sources', page_id) + '.txt' - f = open(filename) - try: - return page_id, f.read() - finally: - f.close() - - - def show_source(self, req, page): - """ - Show the highlighted source for a given page. - """ - return Response(self.get_page_source(page)[1], mimetype='text/plain') - - - def suggest_changes(self, req, page): - """ - Show a "suggest changes" form. - """ - page_id, contents = self.get_page_source(page) - - return Response(render_template(req, 'edit.html', self.globalcontext, dict( - contents=contents, - pagename=page, - doctitle=self.globalcontext['titles'].get(page_id+'.rst') or 'this page', - submiturl=relative_uri('/@edit/'+page+'/', '/@submit/'+page), - ))) - - def _generate_preview(self, page_id, contents): - """ - Generate a preview for suggested changes. - """ - handle, pathname = tempfile.mkstemp() - os.write(handle, contents.encode('utf-8')) - os.close(handle) - - warning_stream = StringIO.StringIO() - env2 = copy.deepcopy(self.env) - destination = StringOutput(encoding='utf-8') - builder = MockBuilder() - builder.config = env2.config - writer = HTMLWriter(builder) - doctree = env2.read_doc(page_id, pathname, save_parsed=False) - doctree = env2.get_and_resolve_doctree(page_id+'.rst', builder, doctree) - doctree.settings = OptionParser(defaults=env2.settings, - components=(writer,)).get_default_values() - doctree.reporter = Reporter(page_id+'.rst', 2, 4, stream=warning_stream) - output = writer.write(doctree, destination) - writer.assemble_parts() - return writer.parts['fragment'] - - - def submit_changes(self, req, page): - """ - Submit the suggested changes as a patch. - """ - if req.method != 'POST': - # only available via POST - raise NotFound() - if req.form.get('cancel'): - # handle cancel requests directly - return RedirectResponse(page) - # raises NotFound if page doesn't exist - page_id, orig_contents = self.get_page_source(page) - author = req.form.get('name') - email = req.form.get('email') - summary = req.form.get('summary') - contents = req.form.get('contents') - fields = (author, email, summary, contents) - - form_error = None - rendered = None - - if not all(fields): - form_error = 'You have to fill out all fields.' - elif not _mail_re.search(email): - form_error = 'You have to provide a valid e-mail address.' - elif req.form.get('homepage') or self.antispam.is_spam(fields): - form_error = 'Your text contains blocked URLs or words.' - else: - if req.form.get('preview'): - rendered = self._generate_preview(page_id, contents) - - else: - asctime = time.asctime() - contents = contents.splitlines() - orig_contents = orig_contents.splitlines() - diffname = 'suggestion on %s by %s <%s>' % (asctime, author, email) - diff = difflib.unified_diff(orig_contents, contents, n=3, - fromfile=page_id, tofile=diffname, - lineterm='') - diff_text = '\n'.join(diff) - try: - mail = Email( - self.config['patch_mail_from'], 'Python Documentation Patches', - self.config['patch_mail_to'], '', - 'Patch for %s by %s' % (page_id, author), - PATCH_MESSAGE % locals(), - self.config['patch_mail_smtp'], - ) - mail.attachments.add_string('patch.diff', diff_text, 'text/x-diff') - mail.send() - except: - import traceback - traceback.print_exc() - # XXX: how to report? - pass - return Response(render_template(req, 'submitted.html', - self.globalcontext, dict( - backlink=relative_uri('/@submit/'+page+'/', page+'/') - ))) - - return Response(render_template(req, 'edit.html', self.globalcontext, dict( - contents=contents, - author=author, - email=email, - summary=summary, - pagename=page, - form_error=form_error, - rendered=rendered, - submiturl=relative_uri('/@edit/'+page+'/', '/@submit/'+page), - ))) - - - def get_settings_page(self, req): - """ - Handle the settings page. - """ - referer = req.environ.get('HTTP_REFERER') or '' - if referer: - base = get_base_uri(req.environ) - if not referer.startswith(base): - referer = '' - else: - referer = referer[len(base):] - referer = referer.split('?')[0] or referer - - if req.method == 'POST': - if req.form.get('cancel'): - if req.form.get('referer'): - return RedirectResponse(req.form['referer']) - return RedirectResponse('') - new_style = req.form.get('design') - if new_style and new_style in known_designs: - req.session['design'] = new_style - new_comments = req.form.get('comments') - if new_comments and new_comments in comments_methods: - req.session['comments'] = new_comments - if req.form.get('goback') and req.form.get('referer'): - return RedirectResponse(req.form['referer']) - # else display the same page again - referer = '' - - context = { - 'known_designs': sorted(known_designs.iteritems()), - 'comments_methods': comments_methods.items(), - 'curdesign': req.session.get('design') or 'default', - 'curcomments': req.session.get('comments') or 'inline', - 'referer': referer, - } - - return Response(render_template(req, 'settings.html', - self.globalcontext, context)) - - - @cached - def get_module_index(self, req): - """ - Get the module index or redirect to a module from the module index. - """ - most_frequent = heapq.nlargest(30, self.freqmodules.iteritems(), - lambda x: x[1]) - if most_frequent: - base_count = most_frequent[-1][1] - most_frequent = [{ - 'name': x[0], - 'size': 100 + math.log((x[1] - base_count) + 1) * 20, - 'count': x[1] - } for x in sorted(most_frequent)] - - showpf = None - newpf = req.args.get('newpf') - sesspf = req.session.get('pf') - if newpf or sesspf: - yield NoCache - if newpf: - req.session['pf'] = showpf = req.args.getlist('pf') - else: - showpf = sesspf - else: - if most_frequent != self.last_most_frequent: - self.cache.pop('@modindex', None) - yield '@modindex' - - filename = path.join(self.data_root, 'modindex.fpickle') - f = open(filename, 'rb') - try: - context = pickle.load(f) - finally: - f.close() - if showpf: - entries = context['modindexentries'] - i = 0 - while i < len(entries): - if entries[i][6]: - for pform in entries[i][6]: - if pform in showpf: - break - else: - del entries[i] - continue - i += 1 - context['freqentries'] = most_frequent - context['showpf'] = showpf or context['platforms'] - self.last_most_frequent = most_frequent - yield render_template(req, 'modindex.html', - self.globalcontext, context) - - def show_comment_form(self, req, page): - """ - Show the "new comment" form. - """ - page_id = self.env.get_real_filename(page)[:-4] - ajax_mode = req.args.get('mode') == 'ajax' - target = req.args.get('target') - page_comment_mode = not target - - form_error = preview = None - title = req.form.get('title', '').strip() - if 'author' in req.form: - author = req.form['author'] - else: - author = req.session.get('author', '') - if 'author_mail' in req.form: - author_mail = req.form['author_mail'] - else: - author_mail = req.session.get('author_mail', '') - comment_body = req.form.get('comment_body', '') - fields = (title, author, author_mail, comment_body) - - if req.method == 'POST': - if req.form.get('preview'): - preview = Comment(page_id, target, title, author, author_mail, - comment_body) - # 'homepage' is a forbidden field to thwart bots - elif req.form.get('homepage') or self.antispam.is_spam(fields): - form_error = 'Your text contains blocked URLs or words.' - else: - if not all(fields): - form_error = 'You have to fill out all fields.' - elif _mail_re.search(author_mail) is None: - form_error = 'You have to provide a valid e-mail address.' - elif len(comment_body) < 20: - form_error = 'You comment is too short ' \ - '(must have at least 20 characters).' - else: - # '|none' can stay since it doesn't include comments - self.cache.pop(page_id + '|inline', None) - self.cache.pop(page_id + '|bottom', None) - comment = Comment(page_id, target, - title, author, author_mail, - comment_body) - comment.save() - req.session['author'] = author - req.session['author_mail'] = author_mail - if ajax_mode: - return JSONResponse({'posted': True, 'error': False, - 'commentID': comment.comment_id}) - return RedirectResponse(comment.url) - - output = render_template(req, '_commentform.html', { - 'ajax_mode': ajax_mode, - 'preview': preview, - 'suggest_url': '@edit/%s/' % page, - 'comments_form': { - 'target': target, - 'title': title, - 'author': author, - 'author_mail': author_mail, - 'comment_body': comment_body, - 'error': form_error - } - }) - - if ajax_mode: - return JSONResponse({ - 'body': output, - 'error': bool(form_error), - 'posted': False - }) - return Response(render_template(req, 'commentform.html', { - 'form': output - })) - - def _insert_comments(self, req, url, context, mode): - """ - Insert inline comments into a page context. - """ - if 'body' not in context: - return - - comment_url = '@comments/%s/' % url - page_id = self.env.get_real_filename(url)[:-4] - tx = context['body'] - all_comments = Comment.get_for_page(page_id) - global_comments = [] - for name, comments in groupby(all_comments, lambda x: x.associated_name): - if not name: - global_comments.extend(comments) - continue - comments = list(comments) - if not comments: - continue - tx = re.sub('' % name, - render_template(req, 'inlinecomments.html', { - 'comments': comments, - 'id': name, - 'comment_url': comment_url, - 'mode': mode}), - tx) - if mode == 'bottom': - global_comments.extend(comments) - if mode == 'inline': - # replace all markers for items without comments - tx = re.sub('', - (lambda match: - render_template(req, 'inlinecomments.html', { - 'id': match.group(1), - 'mode': 'inline', - 'comment_url': comment_url - },)), - tx) - tx += render_template(req, 'comments.html', { - 'comments': global_comments, - 'comment_url': comment_url - }) - context['body'] = tx - - - @cached - def get_page(self, req, url): - """ - Show the requested documentation page or raise an - `NotFound` exception to display a page with close matches. - """ - page_id = self.env.get_real_filename(url)[:-4] - if page_id is None: - raise NotFound(show_keyword_matches=True) - # increment view count of all modules on that page - for modname in self.env.filemodules.get(page_id+'.rst', ()): - self.freqmodules[modname] = self.freqmodules.get(modname, 0) + 1 - # comments enabled? - comments = self.env.metadata[page_id+'.rst'].get('nocomments', False) - - # how does the user want to view comments? - commentmode = comments and req.session.get('comments', 'inline') or '' - - # show "old URL" message? -> no caching possible - oldurl = req.args.get('oldurl') - if oldurl: - yield NoCache - else: - # there must be different cache entries per comment mode - yield page_id + '|' + commentmode - - # cache miss; load the page and render it - filename = path.join(self.data_root, page_id + '.fpickle') - f = open(filename, 'rb') - try: - context = pickle.load(f) - finally: - f.close() - - # add comments to paqe text - if commentmode != 'none': - self._insert_comments(req, url, context, commentmode) - - yield render_template(req, 'page.html', self.globalcontext, context, - {'oldurl': oldurl}) - - - @cached - def get_special_page(self, req, name): - yield '@'+name - filename = path.join(self.data_root, name + '.fpickle') - f = open(filename, 'rb') - try: - context = pickle.load(f) - finally: - f.close() - yield render_template(req, name+'.html', - self.globalcontext, context) - - - def comments_feed(self, req, url): - if url == 'recent': - feed = Feed(req, 'Recent Comments', 'Recent Comments', '') - for comment in Comment.get_recent(): - feed.add_item(comment.title, comment.author, comment.url, - comment.parsed_comment_body, comment.pub_date) - else: - page_id = self.env.get_real_filename(url)[:-4] - doctitle = striptags(self.globalcontext['titles'].get(page_id+'.rst', url)) - feed = Feed(req, 'Comments for "%s"' % doctitle, - 'List of comments for the topic "%s"' % doctitle, url) - for comment in Comment.get_for_page(page_id): - feed.add_item(comment.title, comment.author, comment.url, - comment.parsed_comment_body, comment.pub_date) - return Response(feed.generate(), mimetype='application/rss+xml') - - - def get_error_404(self, req): - """ - Show a simple error 404 page. - """ - return Response(render_template(req, 'not_found.html', self.globalcontext), - status=404) - - - pretty_type = { - 'data': 'module data', - 'cfunction': 'C function', - 'cmember': 'C member', - 'cmacro': 'C macro', - 'ctype': 'C type', - 'cvar': 'C variable', - } - - def get_keyword_matches(self, req, term=None, avoid_fuzzy=False, - is_error_page=False): - """ - Find keyword matches. If there is an exact match, just redirect: - http://docs.python.org/os.path.exists would automatically - redirect to http://docs.python.org/library/os.path/#os.path.exists. - Else, show a page with close matches. - - Module references are processed first so that "os.path" is handled as - a module and not as member of os. - """ - if term is None: - term = req.path.strip('/') - - matches = self.env.find_keyword(term, avoid_fuzzy) - - # if avoid_fuzzy is False matches can be None - if matches is None: - return - - if isinstance(matches, tuple): - url = get_target_uri(matches[1]) - if matches[0] != 'module': - url += '#' + matches[2] - return RedirectResponse(url) - else: - # get some close matches - close_matches = [] - good_matches = 0 - for ratio, type, filename, anchorname, desc in matches: - link = get_target_uri(filename) - if type != 'module': - link += '#' + anchorname - good_match = ratio > 0.75 - good_matches += good_match - close_matches.append({ - 'href': relative_uri(req.path, link), - 'title': anchorname, - 'good_match': good_match, - 'type': self.pretty_type.get(type, type), - 'description': desc, - }) - return Response(render_template(req, 'keyword_not_found.html', { - 'close_matches': close_matches, - 'good_matches_count': good_matches, - 'keyword': term - }, self.globalcontext), status=404) - - - def get_user_stylesheet(self, req): - """ - Stylesheets are exchangeable. Handle them here and - cache them on the server side until server shuts down - and on the client side for 1 hour (not in debug mode). - """ - style = req.session.get('design') - if style not in known_designs: - style = 'default' - - if style in self.generated_stylesheets: - stylesheet = self.generated_stylesheets[style] - else: - stylesheet = [] - for filename in known_designs[style][0]: - f = open(path.join(self.data_root, 'style', filename)) - try: - stylesheet.append(f.read()) - finally: - f.close() - stylesheet = '\n'.join(stylesheet) - if not self.config.get('debug'): - self.generated_stylesheets[style] = stylesheet - - if req.args.get('admin') == 'yes': - f = open(path.join(self.data_root, 'style', 'admin.css')) - try: - stylesheet += '\n' + f.read() - finally: - f.close() - - # XXX: add timestamp based http caching - return Response(stylesheet, mimetype='text/css') - - def __call__(self, environ, start_response): - """ - Dispatch requests. - """ - set_connection(self.db_con) - req = Request(environ) - url = req.path.strip('/') or 'index' - - # check if the environment was updated - new_mtime = path.getmtime(self.buildfile) - if self.buildmtime != new_mtime: - self.load_env(new_mtime) - - try: - if req.path == '/favicon.ico': - # TODO: change this to real favicon? - resp = Response('404 Not Found', status=404) - elif req.path == '/robots.txt': - resp = Response(robots_txt, mimetype='text/plain') - elif not req.path.endswith('/') and req.method == 'GET': - # may be an old URL - if url.endswith('.html'): - resp = handle_html_url(self, url) - else: - # else, require a trailing slash on GET requests - # this ensures nice looking urls and working relative - # links for cached resources. - query = req.environ.get('QUERY_STRING', '') - resp = RedirectResponse(req.path + '/' + (query and '?'+query)) - # index page is special - elif url == 'index': - # presets for settings - if req.args.get('design') and req.args['design'] in known_designs: - req.session['design'] = req.args['design'] - if req.args.get('comments') and req.args['comments'] in comments_methods: - req.session['comments'] = req.args['comments'] - # alias for fuzzy search - if 'q' in req.args: - resp = RedirectResponse('q/%s/' % req.args['q']) - # stylesheet - elif req.args.get('do') == 'stylesheet': - resp = self.get_user_stylesheet(req) - else: - resp = self.get_special_page(req, 'index') - # go to the search page - # XXX: this is currently just a redirect to /q/ which is handled below - elif url == 'search': - resp = self.search(req) - # settings page cannot be cached - elif url == 'settings': - resp = self.get_settings_page(req) - # module index page is special - elif url == 'modindex': - resp = self.get_module_index(req) - # genindex page is special too - elif url == 'genindex': - resp = self.get_special_page(req, 'genindex') - # start the fuzzy search - elif url[:2] == 'q/': - resp = self.get_keyword_matches(req, url[2:]) - # special URLs -- don't forget to add them to robots.py - elif url[0] == '@': - # source view - if url[:8] == '@source/': - resp = self.show_source(req, url[8:]) - # suggest changes view - elif url[:6] == '@edit/': - resp = self.suggest_changes(req, url[6:]) - # suggest changes submit - elif url[:8] == '@submit/': - resp = self.submit_changes(req, url[8:]) - # show that comment form - elif url[:10] == '@comments/': - resp = self.show_comment_form(req, url[10:]) - # comments RSS feed - elif url[:5] == '@rss/': - resp = self.comments_feed(req, url[5:]) - # dispatch requests to the admin panel - elif url == '@admin' or url[:7] == '@admin/': - resp = self.admin_panel.dispatch(req, url[7:]) - else: - raise NotFound() - # everything else is handled as page or fuzzy search - # if a page does not exist. - else: - resp = self.get_page(req, url) - # views can raise a NotFound exception to show an error page. - # Either a real not found page or a similar matches page. - except NotFound, e: - if e.show_keyword_matches: - resp = self.get_keyword_matches(req, is_error_page=True) - else: - resp = self.get_error_404(req) - return resp(environ, start_response) - - -def _check_superuser(app): - """Check if there is a superuser and create one if necessary.""" - if not app.userdb.users: - print 'Warning: you have no user database or no master "admin" account.' - create = raw_input('Do you want to create an admin account now? [y/n] ') - if not create or create.lower().startswith('y'): - import getpass - print 'Creating "admin" user.' - pw1 = getpass.getpass('Enter password: ') - pw2 = getpass.getpass('Enter password again: ') - if pw1 != pw2: - print 'Error: Passwords don\'t match.' - raise SystemExit(1) - app.userdb.set_password('admin', pw1) - app.userdb.privileges['admin'].add('master') - app.userdb.save() - - -def setup_app(config, check_superuser=False): - """ - Create the WSGI application based on a configuration dict. - Handled configuration values so far: - - `data_root_path` - the folder containing the documentation data as generated - by sphinx with the web builder. - """ - app = DocumentationApplication(config) - if check_superuser: - _check_superuser(app) - app = SharedDataMiddleware(app, { - '/static': path.join(config['data_root_path'], 'static') - }) - return app diff --git a/sphinx/web/database.py b/sphinx/web/database.py deleted file mode 100644 index dd1d33224..000000000 --- a/sphinx/web/database.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.database - ~~~~~~~~~~~~~~~~~~~ - - The database connections are thread local. To set the connection - for a thread use the `set_connection` function provided. The - `connect` method automatically sets up new tables and returns a - usable connection which is also set as the connection for the - thread that called that function. - - :copyright: 2007-2008 by Georg Brandl, Armin Ronacher. - :license: BSD. -""" -import time -import sqlite3 -from datetime import datetime -from threading import local - -from sphinx.web.markup import markup - - -_thread_local = local() - - -def connect(path): - """Connect and create tables if required. Also assigns - the connection for the current thread.""" - con = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES) - con.isolation_level = None - - # create tables that do not exist. - for table in tables: - try: - con.execute('select * from %s limit 1;' % table) - except sqlite3.OperationalError: - con.execute(tables[table]) - - set_connection(con) - return con - - -def get_cursor(): - """Return a new cursor.""" - return _thread_local.connection.cursor() - - -def set_connection(con): - """Call this after thread creation to make this connection - the connection for this thread.""" - _thread_local.connection = con - - -#: tables that we use -tables = { - 'comments': ''' - create table comments ( - comment_id integer primary key, - associated_page varchar(200), - associated_name varchar(200), - title varchar(120), - author varchar(200), - author_mail varchar(250), - comment_body text, - pub_date timestamp - );''' -} - - -class Comment(object): - """ - Represents one comment. - """ - - def __init__(self, associated_page, associated_name, title, author, - author_mail, comment_body, pub_date=None): - self.comment_id = None - self.associated_page = associated_page - self.associated_name = associated_name - self.title = title - if pub_date is None: - pub_date = datetime.utcnow() - self.pub_date = pub_date - self.author = author - self.author_mail = author_mail - self.comment_body = comment_body - - @property - def url(self): - return '%s#comment-%s' % ( - self.associated_page, - self.comment_id - ) - - @property - def parsed_comment_body(self): - from sphinx.web.util import get_target_uri - from sphinx.util import relative_uri - uri = get_target_uri(self.associated_page) - def make_rel_link(keyword): - return relative_uri(uri, 'q/%s/' % keyword) - return markup(self.comment_body, make_rel_link) - - def save(self): - """ - Save the comment and use the cursor provided. - """ - cur = get_cursor() - args = (self.associated_page, self.associated_name, self.title, - self.author, self.author_mail, self.comment_body, self.pub_date) - if self.comment_id is None: - cur.execute('''insert into comments (associated_page, associated_name, - title, - author, author_mail, - comment_body, pub_date) - values (?, ?, ?, ?, ?, ?, ?)''', args) - self.comment_id = cur.lastrowid - else: - args += (self.comment_id,) - cur.execute('''update comments set associated_page=?, - associated_name=?, - title=?, author=?, - author_mail=?, comment_body=?, - pub_date=? where comment_id = ?''', args) - cur.close() - - def delete(self): - cur = get_cursor() - cur.execute('delete from comments where comment_id = ?', - (self.comment_id,)) - cur.close() - - @staticmethod - def _make_comment(row): - rv = Comment(*row[1:]) - rv.comment_id = row[0] - return rv - - @staticmethod - def get(comment_id): - cur = get_cursor() - cur.execute('select * from comments where comment_id = ?', (comment_id,)) - row = cur.fetchone() - if row is None: - raise ValueError('comment not found') - try: - return Comment._make_comment(row) - finally: - cur.close() - - @staticmethod - def get_for_page(associated_page, reverse=False): - cur = get_cursor() - cur.execute('''select * from comments where associated_page = ? - order by associated_name, comment_id %s''' % - (reverse and 'desc' or 'asc'), - (associated_page,)) - try: - return [Comment._make_comment(row) for row in cur] - finally: - cur.close() - - @staticmethod - def get_recent(n=10): - cur = get_cursor() - cur.execute('select * from comments order by comment_id desc limit ?', - (n,)) - try: - return [Comment._make_comment(row) for row in cur] - finally: - cur.close() - - @staticmethod - def get_overview(detail_for=None): - cur = get_cursor() - cur.execute('''select distinct associated_page from comments - order by associated_page asc''') - pages = [] - for row in cur: - page_id = row[0] - if page_id == detail_for: - pages.append((page_id, Comment.get_for_page(page_id, True))) - else: - pages.append((page_id, [])) - cur.close() - return pages - - def __repr__(self): - return '' % ( - self.author, - self.associated_page, - self.associated_name, - self.comment_id or 'not saved' - ) diff --git a/sphinx/web/feed.py b/sphinx/web/feed.py deleted file mode 100644 index 4a3bf3896..000000000 --- a/sphinx/web/feed.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.feed - ~~~~~~~~~~~~~~~ - - Nifty module that generates RSS feeds. - - :copyright: 2007-2008 by Armin Ronacher. - :license: BSD. -""" -import time -from datetime import datetime -from xml.dom.minidom import Document -from email.Utils import formatdate - - -def format_rss_date(date): - """ - Pass it a datetime object to receive the string representation - for RSS date fields. - """ - return formatdate(time.mktime(date.timetuple()) + date.microsecond / 1e6) - - -class Feed(object): - """ - Abstract feed creation class. To generate feeds use one of - the subclasses `RssFeed` or `AtomFeed`. - """ - - def __init__(self, req, title, description, link): - self.req = req - self.title = title - self.description = description - self.link = req.make_external_url(link) - self.items = [] - self._last_update = None - - def add_item(self, title, author, link, description, pub_date): - if self._last_update is None or pub_date > self._last_update: - self._last_update = pub_date - date = pub_date or datetime.utcnow() - self.items.append({ - 'title': title, - 'author': author, - 'link': self.req.make_external_url(link), - 'description': description, - 'pub_date': date - }) - - def generate(self): - return self.generate_document().toxml('utf-8') - - def generate_document(self): - doc = Document() - Element = doc.createElement - Text = doc.createTextNode - - rss = doc.appendChild(Element('rss')) - rss.setAttribute('version', '2.0') - - channel = rss.appendChild(Element('channel')) - for key in ('title', 'description', 'link'): - value = getattr(self, key) - channel.appendChild(Element(key)).appendChild(Text(value)) - date = format_rss_date(self._last_update or datetime.utcnow()) - channel.appendChild(Element('pubDate')).appendChild(Text(date)) - - for item in self.items: - d = Element('item') - for key in ('title', 'author', 'link', 'description'): - d.appendChild(Element(key)).appendChild(Text(item[key])) - pub_date = format_rss_date(item['pub_date']) - d.appendChild(Element('pubDate')).appendChild(Text(pub_date)) - d.appendChild(Element('guid')).appendChild(Text(item['link'])) - channel.appendChild(d) - - return doc diff --git a/sphinx/web/mail.py b/sphinx/web/mail.py deleted file mode 100644 index 59f30ff31..000000000 --- a/sphinx/web/mail.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.mail - ~~~~~~~~~~~~~~~ - - A simple module for sending e-mails, based on simplemail.py. - - :copyright: 2004-2007 by Gerold Penz. - :copyright: 2007-2008 by Georg Brandl. - :license: BSD. -""" - -import os.path -import sys -import time -import smtplib -import mimetypes - -from email import Encoders -from email.Header import Header -from email.MIMEText import MIMEText -from email.MIMEMultipart import MIMEMultipart -from email.Utils import formataddr -from email.Utils import formatdate -from email.Message import Message -from email.MIMEAudio import MIMEAudio -from email.MIMEBase import MIMEBase -from email.MIMEImage import MIMEImage - - - -# Exceptions -#---------------------------------------------------------------------- -class SimpleMail_Exception(Exception): - def __str__(self): - return self.__doc__ - -class NoFromAddress_Exception(SimpleMail_Exception): - pass - -class NoToAddress_Exception(SimpleMail_Exception): - pass - -class NoSubject_Exception(SimpleMail_Exception): - pass - -class AttachmentNotFound_Exception(SimpleMail_Exception): - pass - - -class Attachments(object): - def __init__(self): - self._attachments = [] - - def add_filename(self, filename = ''): - self._attachments.append(('file', filename)) - - def add_string(self, filename, text, mimetype): - self._attachments.append(('string', (filename, text, mimetype))) - - def count(self): - return len(self._attachments) - - def get_list(self): - return self._attachments - - -class Recipients(object): - def __init__(self): - self._recipients = [] - - def add(self, address, caption = ''): - self._recipients.append(formataddr((caption, address))) - - def count(self): - return len(self._recipients) - - def __repr__(self): - return str(self._recipients) - - def get_list(self): - return self._recipients - - -class CCRecipients(Recipients): - pass - - -class BCCRecipients(Recipients): - pass - - -class Email(object): - - def __init__( - self, - from_address = "", - from_caption = "", - to_address = "", - to_caption = "", - subject = "", - message = "", - smtp_server = "localhost", - smtp_user = "", - smtp_password = "", - user_agent = "", - reply_to_address = "", - reply_to_caption = "", - use_tls = False, - ): - """ - Initialize the email object - from_address = the email address of the sender - from_caption = the caption (name) of the sender - to_address = the email address of the recipient - to_caption = the caption (name) of the recipient - subject = the subject of the email message - message = the body text of the email message - smtp_server = the ip-address or the name of the SMTP-server - smtp_user = (optional) Login name for the SMTP-Server - smtp_password = (optional) Password for the SMTP-Server - user_agent = (optional) program identification - reply_to_address = (optional) Reply-to email address - reply_to_caption = (optional) Reply-to caption (name) - use_tls = (optional) True, if the connection should use TLS - to encrypt. - """ - - self.from_address = from_address - self.from_caption = from_caption - self.recipients = Recipients() - self.cc_recipients = CCRecipients() - self.bcc_recipients = BCCRecipients() - if to_address: - self.recipients.add(to_address, to_caption) - self.subject = subject - self.message = message - self.smtp_server = smtp_server - self.smtp_user = smtp_user - self.smtp_password = smtp_password - self.attachments = Attachments() - self.content_subtype = "plain" - self.content_charset = "iso-8859-1" - self.header_charset = "us-ascii" - self.statusdict = None - self.user_agent = user_agent - self.reply_to_address = reply_to_address - self.reply_to_caption = reply_to_caption - self.use_tls = use_tls - - - def send(self): - """ - Send the mail. Returns True if successfully sent to at least one - recipient. - """ - - # validation - if len(self.from_address.strip()) == 0: - raise NoFromAddress_Exception - if self.recipients.count() == 0: - if ( - (self.cc_recipients.count() == 0) and - (self.bcc_recipients.count() == 0) - ): - raise NoToAddress_Exception - if len(self.subject.strip()) == 0: - raise NoSubject_Exception - - # assemble - if self.attachments.count() == 0: - msg = MIMEText( - _text = self.message, - _subtype = self.content_subtype, - _charset = self.content_charset - ) - else: - msg = MIMEMultipart() - if self.message: - att = MIMEText( - _text = self.message, - _subtype = self.content_subtype, - _charset = self.content_charset - ) - msg.attach(att) - - # add headers - from_str = formataddr((self.from_caption, self.from_address)) - msg["From"] = from_str - if self.reply_to_address: - reply_to_str = formataddr((self.reply_to_caption, self.reply_to_address)) - msg["Reply-To"] = reply_to_str - if self.recipients.count() > 0: - msg["To"] = ", ".join(self.recipients.get_list()) - if self.cc_recipients.count() > 0: - msg["Cc"] = ", ".join(self.cc_recipients.get_list()) - msg["Date"] = formatdate(time.time()) - msg["User-Agent"] = self.user_agent - try: - msg["Subject"] = Header( - self.subject, self.header_charset - ) - except(UnicodeDecodeError): - msg["Subject"] = Header( - self.subject, self.content_charset - ) - msg.preamble = "You will not see this in a MIME-aware mail reader.\n" - msg.epilogue = "" - - # assemble multipart - if self.attachments.count() > 0: - for typ, info in self.attachments.get_list(): - if typ == 'file': - filename = info - if not os.path.isfile(filename): - raise AttachmentNotFound_Exception, filename - mimetype, encoding = mimetypes.guess_type(filename) - if mimetype is None or encoding is not None: - mimetype = 'application/octet-stream' - if mimetype.startswith('text/'): - fp = file(filename) - else: - fp = file(filename, 'rb') - text = fp.read() - fp.close() - else: - filename, text, mimetype = info - maintype, subtype = mimetype.split('/', 1) - if maintype == 'text': - # Note: we should handle calculating the charset - att = MIMEText(text, _subtype=subtype) - elif maintype == 'image': - att = MIMEImage(text, _subtype=subtype) - elif maintype == 'audio': - att = MIMEAudio(text, _subtype=subtype) - else: - att = MIMEBase(maintype, subtype) - att.set_payload(text) - # Encode the payload using Base64 - Encoders.encode_base64(att) - # Set the filename parameter - att.add_header( - 'Content-Disposition', - 'attachment', - filename = os.path.basename(filename).strip() - ) - msg.attach(att) - - # connect to server - smtp = smtplib.SMTP() - if self.smtp_server: - smtp.connect(self.smtp_server) - else: - smtp.connect() - - # TLS? - if self.use_tls: - smtp.ehlo() - smtp.starttls() - smtp.ehlo() - - # authenticate - if self.smtp_user: - smtp.login(user = self.smtp_user, password = self.smtp_password) - - # send - self.statusdict = smtp.sendmail( - from_str, - ( - self.recipients.get_list() + - self.cc_recipients.get_list() + - self.bcc_recipients.get_list() - ), - msg.as_string() - ) - smtp.close() - - return True diff --git a/sphinx/web/markup.py b/sphinx/web/markup.py deleted file mode 100644 index 481e00f50..000000000 --- a/sphinx/web/markup.py +++ /dev/null @@ -1,239 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.markup - ~~~~~~~~~~~~~~~~~ - - Awfully simple markup used in comments. Syntax: - - `this is some ` - like in HTML - - ``this is like ` just that i can contain backticks`` - like in HTML - - *emphasized* - translates to - - **strong** - translates to - - !!!very important message!!! - use this to mark important or dangerous things. - Translates to - - [[http://www.google.com/]] - Simple link with the link target as caption. If the - URL is relative the provided callback is called to get - the full URL. - - [[http://www.google.com/ go to google]] - Link with "go to google" as caption. - - preformatted code that could by python code - Python code (most of the time), otherwise preformatted. - - cite someone - Like
in HTML. - - :copyright: 2007-2008 by Armin Ronacher. - :license: BSD. -""" -import cgi -import re -from urlparse import urlparse - -from sphinx.highlighting import highlight_block - - -inline_formatting = { - 'escaped_code': ('``', '``'), - 'code': ('`', '`'), - 'strong': ('**', '**'), - 'emphasized': ('*', '*'), - 'important': ('!!!', '!!!'), - 'link': ('[[', ']]'), - 'quote': ('', ''), - 'code_block': ('', ''), - 'paragraph': (r'\n{2,}', None), - 'newline': (r'\\$', None) -} - -simple_formattings = { - 'strong_begin': '', - 'strong_end': '', - 'emphasized_begin': '', - 'emphasized_end': '', - 'important_begin': '', - 'important_end': '', - 'quote_begin': '
', - 'quote_end': '
' -} - -raw_formatting = set(['link', 'code', 'escaped_code', 'code_block']) - -formatting_start_re = re.compile('|'.join( - '(?P<%s>%s)' % (name, end is not None and re.escape(start) or start) - for name, (start, end) - in sorted(inline_formatting.items(), key=lambda x: -len(x[1][0])) -), re.S | re.M) - -formatting_end_res = dict( - (name, re.compile(re.escape(end))) for name, (start, end) - in inline_formatting.iteritems() if end is not None -) - -without_end_tag = set(name for name, (_, end) in inline_formatting.iteritems() - if end is None) - - - -class StreamProcessor(object): - - def __init__(self, stream): - self._pushed = [] - self._stream = stream - - def __iter__(self): - return self - - def next(self): - if self._pushed: - return self._pushed.pop() - return self._stream.next() - - def push(self, token, data): - self._pushed.append((token, data)) - - def get_data(self, drop_needle=False): - result = [] - try: - while True: - token, data = self.next() - if token != 'text': - if not drop_needle: - self.push(token, data) - break - result.append(data) - except StopIteration: - pass - return ''.join(result) - - -class MarkupParser(object): - - def __init__(self, make_rel_url): - self.make_rel_url = make_rel_url - - def tokenize(self, text): - text = '\n'.join(text.splitlines()) - last_pos = 0 - pos = 0 - end = len(text) - stack = [] - text_buffer = [] - - while pos < end: - if stack: - m = formatting_end_res[stack[-1]].match(text, pos) - if m is not None: - if text_buffer: - yield 'text', ''.join(text_buffer) - del text_buffer[:] - yield stack[-1] + '_end', None - stack.pop() - pos = m.end() - continue - - m = formatting_start_re.match(text, pos) - if m is not None: - if text_buffer: - yield 'text', ''.join(text_buffer) - del text_buffer[:] - - for key, value in m.groupdict().iteritems(): - if value is not None: - if key in without_end_tag: - yield key, None - else: - if key in raw_formatting: - regex = formatting_end_res[key] - m2 = regex.search(text, m.end()) - if m2 is None: - yield key, text[m.end():] - else: - yield key, text[m.end():m2.start()] - m = m2 - else: - yield key + '_begin', None - stack.append(key) - break - - if m is None: - break - else: - pos = m.end() - continue - - text_buffer.append(text[pos]) - pos += 1 - - yield 'text', ''.join(text_buffer) - for token in reversed(stack): - yield token + '_end', None - - def stream_to_html(self, text): - stream = StreamProcessor(self.tokenize(text)) - paragraph = [] - result = [] - - def new_paragraph(): - result.append(paragraph[:]) - del paragraph[:] - - for token, data in stream: - if token in simple_formattings: - paragraph.append(simple_formattings[token]) - elif token in ('text', 'escaped_code', 'code'): - if data: - data = cgi.escape(data) - if token in ('escaped_code', 'code'): - data = '%s' % data - paragraph.append(data) - elif token == 'link': - if ' ' in data: - href, caption = data.split(' ', 1) - else: - href = caption = data - protocol = urlparse(href)[0] - nofollow = True - if not protocol: - href = self.make_rel_url(href) - nofollow = False - elif protocol == 'javascript': - href = href[11:] - paragraph.append('%s' % (cgi.escape(href), - nofollow and ' rel="nofollow"' or '', - cgi.escape(caption))) - elif token == 'code_block': - result.append(highlight_block(data, 'python')) - new_paragraph() - elif token == 'paragraph': - new_paragraph() - elif token == 'newline': - paragraph.append('
') - - if paragraph: - result.append(paragraph) - for item in result: - if isinstance(item, list): - if item: - yield '

%s

' % ''.join(item) - else: - yield item - - def to_html(self, text): - return ''.join(self.stream_to_html(text)) - - -def markup(text, make_rel_url=lambda x: './' + x): - return MarkupParser(make_rel_url).to_html(text) diff --git a/sphinx/web/oldurls.py b/sphinx/web/oldurls.py deleted file mode 100644 index d34d1eabf..000000000 --- a/sphinx/web/oldurls.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.oldurls - ~~~~~~~~~~~~~~~~~~ - - Handle old URLs gracefully. - - :copyright: 2007-2008 by Georg Brandl. - :license: BSD. -""" - -import re - -from sphinx.web.wsgiutil import RedirectResponse, NotFound - - -_module_re = re.compile(r'module-(.*)\.html') -_modobj_re = re.compile(r'(.*)-objects\.html') -_modsub_re = re.compile(r'(.*?)-(.*)\.html') - - -special_module_names = { - 'main': '__main__', - 'builtin': '__builtin__', - 'future': '__future__', - 'pycompile': 'py_compile', -} - -tutorial_nodes = [ - '', '', '', - 'appetite', - 'interpreter', - 'introduction', - 'controlflow', - 'datastructures', - 'modules', - 'inputoutput', - 'errors', - 'classes', - 'stdlib', - 'stdlib2', - 'whatnow', - 'interactive', - 'floatingpoint', - '', - 'glossary', -] - - -def handle_html_url(req, url): - def inner(): - # global special pages - if url.endswith('/contents.html'): - return 'contents/' - if url.endswith('/genindex.html'): - return 'genindex/' - if url.endswith('/about.html'): - return 'about/' - if url.endswith('/reporting-bugs.html'): - return 'bugs/' - if url == 'modindex.html' or url.endswith('/modindex.html'): - return 'modindex/' - if url == 'mac/using.html': - return 'howto/pythonmac/' - # library - if url[:4] in ('lib/', 'mac/'): - p = 'library/' - m = _module_re.match(url[4:]) - if m: - mn = m.group(1) - return p + special_module_names.get(mn, mn) - # module sub-pages - m = _modsub_re.match(url[4:]) - if m and not _modobj_re.match(url[4:]): - mn = m.group(1) - return p + special_module_names.get(mn, mn) - # XXX: handle all others - # tutorial - elif url[:4] == 'tut/': - try: - node = int(url[8:].split('.html')[0]) - except ValueError: - pass - else: - if tutorial_nodes[node]: - return 'tutorial/' + tutorial_nodes[node] - # installing: all in one (ATM) - elif url[:5] == 'inst/': - return 'install/' - # no mapping for "documenting Python..." - # nothing found - raise NotFound() - return RedirectResponse('%s?oldurl=1' % inner()) diff --git a/sphinx/web/robots.py b/sphinx/web/robots.py deleted file mode 100644 index b2b26d89b..000000000 --- a/sphinx/web/robots.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.robots - ~~~~~~~~~~~~~~~~~ - - robots.txt - - :copyright: 2007-2008 by Georg Brandl. - :license: BSD. -""" - -robots_txt = """\ -User-agent: * -Disallow: /@source/ -Disallow: /@edit/ -Disallow: /@submit/ -Disallow: /@comments/ -Disallow: /@rss/ -Disallow: /@admin - -User-agent: Googlebot -Disallow: /@source/ -Disallow: /@edit/ -Disallow: /@submit/ -Disallow: /@comments/ -Disallow: /@rss/ -Disallow: /@admin -""" diff --git a/sphinx/web/serve.py b/sphinx/web/serve.py deleted file mode 100644 index 6a94e1c37..000000000 --- a/sphinx/web/serve.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.serve - ~~~~~~~~~~~~~~~~ - - This module optionally wraps the `wsgiref` module so that it reloads code - automatically. Works with any WSGI application but it won't help in non - `wsgiref` environments. Use it only for development. - - :copyright: 2007-2008 by Armin Ronacher, Georg Brandl. - :license: BSD. -""" -import os -import sys -import time -import thread - - -def reloader_loop(extra_files): - """When this function is run from the main thread, it will force other - threads to exit when any modules currently loaded change. - - :param extra_files: a list of additional files it should watch. - """ - mtimes = {} - while True: - for filename in filter(None, [getattr(module, '__file__', None) - for module in sys.modules.values()] + - extra_files): - while not os.path.isfile(filename): - filename = os.path.dirname(filename) - if not filename: - break - if not filename: - continue - - if filename[-4:] in ('.pyc', '.pyo'): - filename = filename[:-1] - - mtime = os.stat(filename).st_mtime - if filename not in mtimes: - mtimes[filename] = mtime - continue - if mtime > mtimes[filename]: - sys.exit(3) - time.sleep(1) - - -def restart_with_reloader(): - """Spawn a new Python interpreter with the same arguments as this one, - but running the reloader thread.""" - while True: - print '* Restarting with reloader...' - args = [sys.executable] + sys.argv - if sys.platform == 'win32': - args = ['"%s"' % arg for arg in args] - new_environ = os.environ.copy() - new_environ['RUN_MAIN'] = 'true' - exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ) - if exit_code != 3: - return exit_code - - -def run_with_reloader(main_func, extra_watch): - """ - Run the given function in an independent Python interpreter. - """ - if os.environ.get('RUN_MAIN') == 'true': - thread.start_new_thread(main_func, ()) - try: - reloader_loop(extra_watch) - except KeyboardInterrupt: - return - try: - sys.exit(restart_with_reloader()) - except KeyboardInterrupt: - pass - - -def run_simple(hostname, port, make_app, use_reloader=False, - extra_files=None): - """ - Start an application using wsgiref and with an optional reloader. - """ - from wsgiref.simple_server import make_server - def inner(): - application = make_app() - print '* Startup complete.' - srv = make_server(hostname, port, application) - try: - srv.serve_forever() - except KeyboardInterrupt: - pass - if os.environ.get('RUN_MAIN') != 'true': - print '* Running on http://%s:%d/' % (hostname, port) - if use_reloader: - run_with_reloader(inner, extra_files or []) - else: - inner() diff --git a/sphinx/web/static/admin.css b/sphinx/web/static/admin.css deleted file mode 100644 index a25b77fa1..000000000 --- a/sphinx/web/static/admin.css +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Sphinx Admin Panel - */ - -div.admin { - margin: 0 -20px -30px -20px; - padding: 0 20px 10px 20px; - background-color: #f2f2f2; - color: black; -} - -div.admin a { - color: #333; - text-decoration: underline; -} - -div.admin a:hover { - color: black; -} - -div.admin h1, -div.admin h2 { - background-color: #555; - border-bottom: 1px solid #222; - color: white; -} - -div.admin form form { - display: inline; -} - -div.admin input, div.admin textarea { - font-family: 'Bitstream Vera Sans', 'Arial', sans-serif; - font-size: 13px; - color: #333; - padding: 2px; - background-color: #fff; - border: 1px solid #aaa; -} - -div.admin input[type="reset"], -div.admin input[type="submit"] { - cursor: pointer; - font-weight: bold; - padding: 2px; -} - -div.admin input[type="reset"]:hover, -div.admin input[type="submit"]:hover { - border: 1px solid #333; -} - -div.admin div.actions { - margin: 10px 0 0 0; - padding: 5px; - background-color: #aaa; - border: 1px solid #777; -} - -div.admin div.error { - margin: 10px 0 0 0; - padding: 5px; - border: 2px solid #222; - background-color: #ccc; - font-weight: bold; -} - -div.admin div.dialog { - background-color: #ccc; - margin: 10px 0 10px 0; -} - -div.admin div.dialog h2 { - margin: 0; - font-size: 18px; - padding: 4px 10px 4px 10px; -} - -div.admin div.dialog div.text { - padding: 10px; -} - -div.admin div.dialog div.buttons { - padding: 5px 10px 5px 10px; -} - -div.admin table.mapping { - width: 100%; - border: 1px solid #999; - border-collapse: collapse; - background-color: #aaa; -} - -div.admin table.mapping th { - background-color: #ddd; - border-bottom: 1px solid #888; - padding: 5px; -} - -div.admin table.mapping th.recent_comments { - background-color: #c5cba4; -} - -div.admin table.mapping, -div.admin table.mapping a { - color: black; -} - -div.admin table.mapping td { - border: 1px solid #888; - border-left: none; - border-right: none; - text-align: left; - line-height: 24px; - padding: 0 5px 0 5px; -} - -div.admin table.mapping tr:hover { - background-color: #888; -} - -div.admin table.mapping td.username { - width: 180px; -} - -div.admin table.mapping td.pub_date { - font-style: italic; - text-align: right; -} - -div.admin table.mapping td.groups input { - width: 100%; -} - -div.admin table.mapping td.actions input { - padding: 0; -} - -div.admin table.mapping .actions { - text-align: right; - width: 70px; -} - -div.admin table.mapping span.meta { - font-size: 11px; - color: #222; -} - -div.admin table.mapping span.meta a { - color: #222; -} - -div.admin div.detail_form dt { - clear: both; - float: left; - width: 110px; -} - -div.admin div.detail_form textarea { - width: 98%; - height: 160px; -} diff --git a/sphinx/web/static/comment.png b/sphinx/web/static/comment.png deleted file mode 100644 index 5219131f2..000000000 Binary files a/sphinx/web/static/comment.png and /dev/null differ diff --git a/sphinx/web/static/hovercomment.png b/sphinx/web/static/hovercomment.png deleted file mode 100644 index 5f2461f80..000000000 Binary files a/sphinx/web/static/hovercomment.png and /dev/null differ diff --git a/sphinx/web/static/nocomment.png b/sphinx/web/static/nocomment.png deleted file mode 100644 index 954a2454a..000000000 Binary files a/sphinx/web/static/nocomment.png and /dev/null differ diff --git a/sphinx/web/static/preview.png b/sphinx/web/static/preview.png deleted file mode 100644 index 0c5df6eea..000000000 Binary files a/sphinx/web/static/preview.png and /dev/null differ diff --git a/sphinx/web/userdb.py b/sphinx/web/userdb.py deleted file mode 100644 index 39e6479bc..000000000 --- a/sphinx/web/userdb.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.userdb - ~~~~~~~~~~~~~~~~~ - - A module that provides pythonic access to the `docusers` file - that stores users and their passwords so that they can gain access - to the administration system. - - :copyright: 2007-2008 by Armin Ronacher. - :license: BSD. -""" - -from os import path -from hashlib import sha1 -from random import choice, randrange - - -def gen_password(length=8, add_numbers=True, mix_case=True, - add_special_char=True): - """ - Generate a pronounceable password. - """ - if length <= 0: - raise ValueError('requested password of length <= 0') - consonants = 'bcdfghjklmnprstvwz' - vowels = 'aeiou' - if mix_case: - consonants = consonants * 2 + consonants.upper() - vowels = vowels * 2 + vowels.upper() - pw = ''.join([choice(consonants) + - choice(vowels) + - choice(consonants + vowels) for _ - in xrange(length // 3 + 1)])[:length] - if add_numbers: - n = length // 3 - if n > 0: - pw = pw[:-n] - for _ in xrange(n): - pw += choice('0123456789') - if add_special_char: - tmp = randrange(0, len(pw)) - l1 = pw[:tmp] - l2 = pw[tmp:] - if max(len(l1), len(l2)) == len(l1): - l1 = l1[:-1] - else: - l2 = l2[:-1] - return l1 + choice('#$&%?!') + l2 - return pw - - -class UserDatabase(object): - - def __init__(self, filename): - self.filename = filename - self.users = {} - self.privileges = {} - if path.exists(filename): - f = open(filename) - try: - for line in f: - line = line.strip() - if line and line[0] != '#': - parts = line.split(':') - self.users[parts[0]] = parts[1] - self.privileges.setdefault(parts[0], set()).update( - x for x in parts[2].split(',') if x) - finally: - f.close() - - def set_password(self, user, password): - """Encode the password for a user (also adds users).""" - self.users[user] = sha1('%s|%s' % (user, password)).hexdigest() - - def add_user(self, user): - """Add a new user and return the generated password.""" - pw = gen_password(8, add_special_char=False) - self.set_password(user, pw) - self.privileges[user].clear() - return pw - - def check_password(self, user, password): - return user in self.users and \ - self.users[user] == sha1('%s|%s' % (user, password)).hexdigest() - - def save(self): - f = open(self.filename, 'w') - try: - for username, password in self.users.iteritems(): - privileges = ','.join(self.privileges.get(username, ())) - f.write('%s:%s:%s\n' % (username, password, privileges)) - finally: - f.close() diff --git a/sphinx/web/util.py b/sphinx/web/util.py deleted file mode 100644 index c69c96dfd..000000000 --- a/sphinx/web/util.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.util - ~~~~~~~~~~~~~~~ - - Miscellaneous utilities. - - :copyright: 2007-2008 by Georg Brandl. - :license: BSD. -""" - -import re -from os import path - -from sphinx.util import relative_uri -from sphinx._jinja import Environment, FileSystemLoader - - -def get_target_uri(source_filename): - """Get the web-URI for a given reST file name (without extension).""" - if source_filename == 'index': - return '' - if source_filename.endswith('/index'): - return source_filename[:-5] # up to / - return source_filename + '/' - - -# ------------------------------------------------------------------------------ -# Setup the templating environment - -templates_path = path.join(path.dirname(__file__), '..', 'templates') -jinja_env = Environment(loader=FileSystemLoader(templates_path, - use_memcache=True), - friendly_traceback=True) - -def do_datetime_format(): - def wrapped(env, ctx, value): - return value.strftime('%a, %d %b %Y %H:%M') - return wrapped - -jinja_env.filters['datetimeformat'] = do_datetime_format - - -_striptags_re = re.compile(r'(|<[^>]+>)') - -def striptags(text): - return ' '.join(_striptags_re.sub('', text).split()) - - -def render_template(req, template_name, *contexts): - context = {} - for ctx in contexts: - context.update(ctx) - tmpl = jinja_env.get_template(template_name) - - path = req.path.lstrip('/') - if not path[-1:] == '/': - path += '/' - def relative_path_to(otheruri, resource=False): - if not resource: - otheruri = get_target_uri(otheruri) - return relative_uri(path, otheruri) - context['pathto'] = relative_path_to - - # add it here a second time for templates that don't - # get the builder information from the environment (such as search) - context['builder'] = 'web' - context['req'] = req - - return tmpl.render(context) - - -class lazy_property(object): - """ - Descriptor implementing a "lazy property", i.e. the function - calculating the property value is called only once. - """ - - def __init__(self, func, name=None, doc=None): - self._func = func - self._name = name or func.func_name - self.__doc__ = doc or func.__doc__ - - def __get__(self, obj, objtype=None): - if obj is None: - return self - value = self._func(obj) - setattr(obj, self._name, value) - return value - - -class blackhole_dict(dict): - def __setitem__(self, key, value): - pass diff --git a/sphinx/web/webconf.py b/sphinx/web/webconf.py deleted file mode 100644 index 8a567c451..000000000 --- a/sphinx/web/webconf.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Sphinx documentation web application configuration file -# - -# Where the server listens. -listen_addr = 'localhost' -listen_port = 3000 - -# How patch mails are sent. -patch_mail_from = 'patches@localhost' -patch_mail_to = 'docs@localhost' -patch_mail_smtp = 'localhost' diff --git a/sphinx/web/wsgiutil.py b/sphinx/web/wsgiutil.py deleted file mode 100644 index 9a74d92c8..000000000 --- a/sphinx/web/wsgiutil.py +++ /dev/null @@ -1,705 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.web.wsgiutil - ~~~~~~~~~~~~~~~~~~~ - - To avoid further dependencies this module collects some of the - classes werkzeug provides and use in other views. - - :copyright: 2007-2008 by Armin Ronacher. - :license: BSD. -""" - -import cgi -import urllib -import cPickle as pickle -import tempfile -from os import path -from time import gmtime, time, asctime -from random import random -from Cookie import SimpleCookie -from hashlib import sha1 -from datetime import datetime -from cStringIO import StringIO - -from sphinx.web.util import lazy_property -from sphinx.util.json import dump_json - - -HTTP_STATUS_CODES = { - 100: 'CONTINUE', - 101: 'SWITCHING PROTOCOLS', - 102: 'PROCESSING', - 200: 'OK', - 201: 'CREATED', - 202: 'ACCEPTED', - 203: 'NON-AUTHORITATIVE INFORMATION', - 204: 'NO CONTENT', - 205: 'RESET CONTENT', - 206: 'PARTIAL CONTENT', - 207: 'MULTI STATUS', - 300: 'MULTIPLE CHOICES', - 301: 'MOVED PERMANENTLY', - 302: 'FOUND', - 303: 'SEE OTHER', - 304: 'NOT MODIFIED', - 305: 'USE PROXY', - 306: 'RESERVED', - 307: 'TEMPORARY REDIRECT', - 400: 'BAD REQUEST', - 401: 'UNAUTHORIZED', - 402: 'PAYMENT REQUIRED', - 403: 'FORBIDDEN', - 404: 'NOT FOUND', - 405: 'METHOD NOT ALLOWED', - 406: 'NOT ACCEPTABLE', - 407: 'PROXY AUTHENTICATION REQUIRED', - 408: 'REQUEST TIMEOUT', - 409: 'CONFLICT', - 410: 'GONE', - 411: 'LENGTH REQUIRED', - 412: 'PRECONDITION FAILED', - 413: 'REQUEST ENTITY TOO LARGE', - 414: 'REQUEST-URI TOO LONG', - 415: 'UNSUPPORTED MEDIA TYPE', - 416: 'REQUESTED RANGE NOT SATISFIABLE', - 417: 'EXPECTATION FAILED', - 500: 'INTERNAL SERVER ERROR', - 501: 'NOT IMPLEMENTED', - 502: 'BAD GATEWAY', - 503: 'SERVICE UNAVAILABLE', - 504: 'GATEWAY TIMEOUT', - 505: 'HTTP VERSION NOT SUPPORTED', - 506: 'VARIANT ALSO VARIES', - 507: 'INSUFFICIENT STORAGE', - 510: 'NOT EXTENDED' -} - -SID_COOKIE_NAME = 'python_doc_sid' - - -# ------------------------------------------------------------------------------ -# Support for HTTP parameter parsing, requests and responses - - -class _StorageHelper(cgi.FieldStorage): - """ - Helper class used by `Request` to parse submitted file and - form data. Don't use this class directly. - """ - - FieldStorageClass = cgi.FieldStorage - - def __init__(self, environ, get_stream): - cgi.FieldStorage.__init__(self, - fp=environ['wsgi.input'], - environ={ - 'REQUEST_METHOD': environ['REQUEST_METHOD'], - 'CONTENT_TYPE': environ['CONTENT_TYPE'], - 'CONTENT_LENGTH': environ['CONTENT_LENGTH'] - }, - keep_blank_values=True - ) - self.get_stream = get_stream - - def make_file(self, binary=None): - return self.get_stream() - - -class MultiDict(dict): - """ - A dict that takes a list of multiple values as only argument - in order to store multiple values per key. - """ - - def __init__(self, mapping=()): - if isinstance(mapping, MultiDict): - dict.__init__(self, mapping.lists()) - elif isinstance(mapping, dict): - tmp = {} - for key, value in mapping: - tmp[key] = [value] - dict.__init__(self, tmp) - else: - tmp = {} - for key, value in mapping: - tmp.setdefault(key, []).append(value) - dict.__init__(self, tmp) - - def __getitem__(self, key): - """ - Return the first data value for this key; - raises KeyError if not found. - """ - return dict.__getitem__(self, key)[0] - - def __setitem__(self, key, value): - """Set an item as list.""" - dict.__setitem__(self, key, [value]) - - def get(self, key, default=None): - """Return the default value if the requested data doesn't exist""" - try: - return self[key] - except KeyError: - return default - - def getlist(self, key): - """Return an empty list if the requested data doesn't exist""" - try: - return dict.__getitem__(self, key) - except KeyError: - return [] - - def setlist(self, key, new_list): - """Set new values for an key.""" - dict.__setitem__(self, key, list(new_list)) - - def setdefault(self, key, default=None): - if key not in self: - self[key] = default - else: - default = self[key] - return default - - def setlistdefault(self, key, default_list=()): - if key not in self: - default_list = list(default_list) - dict.__setitem__(self, key, default_list) - else: - default_list = self.getlist(key) - return default_list - - def items(self): - """ - Return a list of (key, value) pairs, where value is the last item in - the list associated with the key. - """ - return [(key, self[key]) for key in self.iterkeys()] - - lists = dict.items - - def values(self): - """Returns a list of the last value on every key list.""" - return [self[key] for key in self.iterkeys()] - - listvalues = dict.values - - def iteritems(self): - for key, values in dict.iteritems(self): - yield key, values[0] - - iterlists = dict.iteritems - - def itervalues(self): - for values in dict.itervalues(self): - yield values[0] - - iterlistvalues = dict.itervalues - - def copy(self): - """Return a shallow copy of this object.""" - return self.__class__(self) - - def update(self, other_dict): - """update() extends rather than replaces existing key lists.""" - if isinstance(other_dict, MultiDict): - for key, value_list in other_dict.iterlists(): - self.setlistdefault(key, []).extend(value_list) - elif isinstance(other_dict, dict): - for key, value in other_dict.items(): - self.setlistdefault(key, []).append(value) - else: - for key, value in other_dict: - self.setlistdefault(key, []).append(value) - - def pop(self, *args): - """Pop the first item for a list on the dict.""" - return dict.pop(self, *args)[0] - - def popitem(self): - """Pop an item from the dict.""" - item = dict.popitem(self) - return (item[0], item[1][0]) - - poplist = dict.pop - popitemlist = dict.popitem - - def __repr__(self): - tmp = [] - for key, values in self.iterlists(): - for value in values: - tmp.append((key, value)) - return '%s(%r)' % (self.__class__.__name__, tmp) - - -class Headers(object): - """ - An object that stores some headers. - """ - - def __init__(self, defaults=None): - self._list = [] - if isinstance(defaults, dict): - for key, value in defaults.iteritems(): - if isinstance(value, (tuple, list)): - for v in value: - self._list.append((key, v)) - else: - self._list.append((key, value)) - elif defaults is not None: - for key, value in defaults: - self._list.append((key, value)) - - def __getitem__(self, key): - ikey = key.lower() - for k, v in self._list: - if k.lower() == ikey: - return v - raise KeyError(key) - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - def getlist(self, key): - ikey = key.lower() - result = [] - for k, v in self._list: - if k.lower() == ikey: - result.append((k, v)) - return result - - def setlist(self, key, values): - del self[key] - self.addlist(key, values) - - def addlist(self, key, values): - self._list.extend(values) - - def lists(self, lowercased=False): - if not lowercased: - return self._list[:] - return [(x.lower(), y) for x, y in self._list] - - def iterlists(self, lowercased=False): - for key, value in self._list: - if lowercased: - key = key.lower() - yield key, value - - def iterkeys(self): - for key, _ in self.iterlists(): - yield key - - def itervalues(self): - for _, value in self.iterlists(): - yield value - - def keys(self): - return list(self.iterkeys()) - - def values(self): - return list(self.itervalues()) - - def __delitem__(self, key): - key = key.lower() - new = [] - for k, v in self._list: - if k != key: - new.append((k, v)) - self._list[:] = new - - remove = __delitem__ - - def __contains__(self, key): - key = key.lower() - for k, v in self._list: - if k.lower() == key: - return True - return False - - has_key = __contains__ - - def __iter__(self): - return iter(self._list) - - def add(self, key, value): - """add a new header tuple to the list""" - self._list.append((key, value)) - - def clear(self): - """clears all headers""" - del self._list[:] - - def set(self, key, value): - """remove all header tuples for key and add - a new one - """ - del self[key] - self.add(key, value) - - __setitem__ = set - - def to_list(self, charset): - """Create a str only list of the headers.""" - result = [] - for k, v in self: - if isinstance(v, unicode): - v = v.encode(charset) - else: - v = str(v) - result.append((k, v)) - return result - - def copy(self): - return self.__class__(self._list) - - def __repr__(self): - return '%s(%r)' % ( - self.__class__.__name__, - self._list - ) - - -class Session(dict): - - def __init__(self, sid): - self.sid = sid - if sid is not None: - if path.exists(self.filename): - f = open(self.filename, 'rb') - try: - self.update(pickle.load(f)) - finally: - f.close() - self._orig = dict(self) - - @property - def filename(self): - if self.sid is not None: - return path.join(tempfile.gettempdir(), '__pydoc_sess' + self.sid) - - @property - def worth_saving(self): - return self != self._orig - - def save(self): - if self.sid is None: - self.sid = sha1('%s|%s' % (time(), random())).hexdigest() - f = open(self.filename, 'wb') - try: - pickle.dump(dict(self), f, pickle.HIGHEST_PROTOCOL) - finally: - f.close() - self._orig = dict(self) - - -class Request(object): - charset = 'utf-8' - - def __init__(self, environ): - self.environ = environ - self.environ['werkzeug.request'] = self - self.session = Session(self.cookies.get(SID_COOKIE_NAME)) - self.user = self.session.get('user') - - def login(self, user): - self.user = self.session['user'] = user - - def logout(self): - self.user = None - self.session.pop('user', None) - - def _get_file_stream(self): - """Called to get a stream for the file upload. - - This must provide a file-like class with `read()`, `readline()` - and `seek()` methods that is both writeable and readable.""" - return tempfile.TemporaryFile('w+b') - - def _load_post_data(self): - """Method used internally to retrieve submitted data.""" - self._data = '' - post = [] - files = [] - if self.environ['REQUEST_METHOD'] in ('POST', 'PUT'): - storage = _StorageHelper(self.environ, self._get_file_stream) - for key in storage.keys(): - values = storage[key] - if not isinstance(values, list): - values = [values] - for item in values: - if getattr(item, 'filename', None) is not None: - fn = item.filename.decode(self.charset, 'ignore') - # fix stupid IE bug - if len(fn) > 1 and fn[1] == ':' and '\\' in fn: - fn = fn[fn.index('\\') + 1:] - files.append((key, FileStorage(key, fn, item.type, - item.length, item.file))) - else: - post.append((key, item.value.decode(self.charset, - 'ignore'))) - self._form = MultiDict(post) - self._files = MultiDict(files) - - def read(self, *args): - if not hasattr(self, '_buffered_stream'): - self._buffered_stream = StringIO(self.data) - return self._buffered_stream.read(*args) - - def readline(self, *args): - if not hasattr(self, '_buffered_stream'): - self._buffered_stream = StringIO(self.data) - return self._buffered_stream.readline(*args) - - def make_external_url(self, path): - url = self.environ['wsgi.url_scheme'] + '://' - if 'HTTP_HOST' in self.environ: - url += self.environ['HTTP_HOST'] - else: - url += self.environ['SERVER_NAME'] - if (self.environ['wsgi.url_scheme'], self.environ['SERVER_PORT']) not \ - in (('https', '443'), ('http', '80')): - url += ':' + self.environ['SERVER_PORT'] - - url += urllib.quote(self.environ.get('SCRIPT_INFO', '').rstrip('/')) - if not path.startswith('/'): - path = '/' + path - return url + path - - def args(self): - """URL parameters""" - items = [] - qs = self.environ.get('QUERY_STRING', '') - for key, values in cgi.parse_qs(qs, True).iteritems(): - for value in values: - value = value.decode(self.charset, 'ignore') - items.append((key, value)) - return MultiDict(items) - args = lazy_property(args) - - def data(self): - """raw value of input stream.""" - if not hasattr(self, '_data'): - self._load_post_data() - return self._data - data = lazy_property(data) - - def form(self): - """form parameters.""" - if not hasattr(self, '_form'): - self._load_post_data() - return self._form - form = lazy_property(form) - - def files(self): - """File uploads.""" - if not hasattr(self, '_files'): - self._load_post_data() - return self._files - files = lazy_property(files) - - def cookies(self): - """Stored Cookies.""" - cookie = SimpleCookie() - cookie.load(self.environ.get('HTTP_COOKIE', '')) - result = {} - for key, value in cookie.iteritems(): - result[key] = value.value.decode(self.charset, 'ignore') - return result - cookies = lazy_property(cookies) - - def method(self): - """Request method.""" - return self.environ['REQUEST_METHOD'] - method = property(method, doc=method.__doc__) - - def path(self): - """Requested path.""" - path = '/' + (self.environ.get('PATH_INFO') or '').lstrip('/') - path = path.decode(self.charset, self.charset) - parts = path.replace('+', ' ').split('/') - return u'/'.join(p for p in parts if p != '..') - path = lazy_property(path) - - -class Response(object): - charset = 'utf-8' - default_mimetype = 'text/html' - - def __init__(self, response=None, headers=None, status=200, mimetype=None): - if response is None: - self.response = [] - elif isinstance(response, basestring): - self.response = [response] - else: - self.response = iter(response) - if not headers: - self.headers = Headers() - elif isinstance(headers, Headers): - self.headers = headers - else: - self.headers = Headers(headers) - if mimetype is None and 'Content-Type' not in self.headers: - mimetype = self.default_mimetype - if mimetype is not None: - if 'charset=' not in mimetype and mimetype.startswith('text/'): - mimetype += '; charset=' + self.charset - self.headers['Content-Type'] = mimetype - self.status = status - self._cookies = None - - def write(self, value): - if not isinstance(self.response, list): - raise RuntimeError('cannot write to streaming response') - self.write = self.response.append - self.response.append(value) - - def set_cookie(self, key, value='', max_age=None, expires=None, - path='/', domain=None, secure=None): - if self._cookies is None: - self._cookies = SimpleCookie() - if isinstance(value, unicode): - value = value.encode(self.charset) - self._cookies[key] = value - if max_age is not None: - self._cookies[key]['max-age'] = max_age - if expires is not None: - if isinstance(expires, basestring): - self._cookies[key]['expires'] = expires - expires = None - elif isinstance(expires, datetime): - expires = expires.utctimetuple() - elif not isinstance(expires, (int, long)): - expires = gmtime(expires) - else: - raise ValueError('datetime or integer required') - month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', - 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][expires.tm_mon - 1] - day = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', - 'Friday', 'Saturday', 'Sunday'][expires.tm_wday] - date = '%02d-%s-%s' % ( - expires.tm_mday, month, str(expires.tm_year)[-2:] - ) - d = '%s, %s %02d:%02d:%02d GMT' % (day, date, expires.tm_hour, - expires.tm_min, expires.tm_sec) - self._cookies[key]['expires'] = d - if path is not None: - self._cookies[key]['path'] = path - if domain is not None: - self._cookies[key]['domain'] = domain - if secure is not None: - self._cookies[key]['secure'] = secure - - def delete_cookie(self, key): - if self._cookies is None: - self._cookies = SimpleCookie() - if key not in self._cookies: - self._cookies[key] = '' - self._cookies[key]['max-age'] = 0 - - def __call__(self, environ, start_response): - req = environ['werkzeug.request'] - if req.session.worth_saving: - req.session.save() - self.set_cookie(SID_COOKIE_NAME, req.session.sid) - - headers = self.headers.to_list(self.charset) - if self._cookies is not None: - for morsel in self._cookies.values(): - headers.append(('Set-Cookie', morsel.output(header=''))) - status = '%d %s' % (self.status, HTTP_STATUS_CODES[self.status]) - - charset = self.charset or 'ascii' - start_response(status, headers) - for item in self.response: - if isinstance(item, unicode): - yield item.encode(charset) - else: - yield str(item) - -def get_base_uri(environ): - url = environ['wsgi.url_scheme'] + '://' - if 'HTTP_HOST' in environ: - url += environ['HTTP_HOST'] - else: - url += environ['SERVER_NAME'] - if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \ - in (('https', '443'), ('http', '80')): - url += ':' + environ['SERVER_PORT'] - url += urllib.quote(environ.get('SCRIPT_INFO', '').rstrip('/')) - return url - - -class RedirectResponse(Response): - - def __init__(self, target_url, code=302): - if not target_url.startswith('/'): - target_url = '/' + target_url - self.target_url = target_url - super(RedirectResponse, self).__init__('Moved...', status=code) - - def __call__(self, environ, start_response): - url = get_base_uri(environ) + self.target_url - self.headers['Location'] = url - return super(RedirectResponse, self).__call__(environ, start_response) - - -class JSONResponse(Response): - - def __init__(self, data): - assert not isinstance(data, list), 'list unsafe for json dumping' - super(JSONResponse, self).__init__(dump_json(data), mimetype='text/javascript') - - -class SharedDataMiddleware(object): - """ - Redirects calls to an folder with static data. - """ - - def __init__(self, app, exports): - self.app = app - self.exports = exports - self.cache = {} - - def serve_file(self, filename, start_response): - from mimetypes import guess_type - guessed_type = guess_type(filename) - mime_type = guessed_type[0] or 'text/plain' - expiry = time() + 3600 # one hour - expiry = asctime(gmtime(expiry)) - start_response('200 OK', [('Content-Type', mime_type), - ('Cache-Control', 'public'), - ('Expires', expiry)]) - f = open(filename, 'rb') - try: - return [f.read()] - finally: - f.close() - - def __call__(self, environ, start_response): - p = environ.get('PATH_INFO', '') - if p in self.cache: - return self.serve_file(self.cache[p], start_response) - for search_path, file_path in self.exports.iteritems(): - if not search_path.endswith('/'): - search_path += '/' - if p.startswith(search_path): - real_path = path.join(file_path, p[len(search_path):]) - if path.exists(real_path) and path.isfile(real_path): - self.cache[p] = real_path - return self.serve_file(real_path, start_response) - return self.app(environ, start_response) - - -class NotFound(Exception): - """ - Raise to display the 404 error page. - """ - - def __init__(self, show_keyword_matches=False): - self.show_keyword_matches = show_keyword_matches - Exception.__init__(self, show_keyword_matches)