# -*- coding: utf-8 -*- from __future__ import unicode_literals from flask import (g, session, request, render_template, flash, redirect, url_for, current_app) from functools import wraps import codecs import uuid import datetime import time import re import importlib import smtplib import markdown import json import urlparse import traceback import socket from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from rophako.log import logger from rophako.settings import Config def login_required(f): """Wrapper for pages that require a logged-in user.""" @wraps(f) def decorated_function(*args, **kwargs): if not g.info["session"]["login"]: session["redirect_url"] = request.url flash("You must be logged in to do that!") return redirect(url_for("account.login")) return f(*args, **kwargs) return decorated_function def admin_required(f): """Wrapper for admin-only pages. Implies login_required.""" @wraps(f) def decorated_function(*args, **kwargs): if not g.info["session"]["login"]: # Not even logged in? session["redirect_url"] = request.url flash("You must be logged in to do that!") return redirect(url_for("account.login")) if g.info["session"]["role"] != "admin": logger.warning("User tried to access an Admin page, but wasn't allowed!") return redirect(url_for("index")) return f(*args, **kwargs) return decorated_function def ajax_response(status, msg): """Return a standard JSON response.""" status = "ok" if status else "error" return json.dumps(dict( status=status, msg=msg, )) def template(name, **kwargs): """Render a template to the browser.""" html = render_template(name, **kwargs) # Get the elapsed time for the request. time_elapsed = "%.03f" % (time.time() - g.info["time"]) html = re.sub(r'\%time_elapsed\%', time_elapsed, html) return html def markdown_template(path): """Render a Markdown page to the browser. The first line in the Markdown page should be an H1 header beginning with the # sign. This will set the page's to match the header value. Pages can include lines that begin with the keyword `:meta` to apply meta information to control the Markdown parser. Supported meta lines and examples: To 'blacklist' extensions, i.e. to turn off line breaks inside a paragraph getting translated into a <br> tag (the key is the minus sign): :meta extensions -nl2br To add an extension, i.e. the abbreviations from PHP Markdown Extra: :meta extensions abbr""" # The path is the absolute path to the Markdown file, so open it directly. fh = codecs.open(path, "r", "utf-8") body = fh.read() fh.close() # Look for meta information in the file. lines = body.split("\n") content = list() # New set of lines, without meta info. extensions = set() blacklist = set() # Blacklisted extensions for line in lines: if line.startswith(":meta"): parts = line.split(" ") if len(parts) >= 3: # Supported meta commands. if parts[1] == "extensions": # Extension toggles. for extension in parts[2:]: if extension.startswith("-"): extension = extension[1:] blacklist.add(extension) else: extensions.add(extension) else: content.append(line) # Extract a title from the first line. first = content[0] if first.startswith("#"): first = first[1:].strip() rendered = render_markdown("\n".join(content), extensions=extensions, blacklist=blacklist, ) return template("markdown.inc.html", title=first, markdown=rendered, ) def render_markdown(body, html_escape=True, extensions=None, blacklist=None): """Render a block of Markdown text. This will default to escaping literal HTML characters. Set `html_escape=False` to trust HTML. * extensions should be a set() of extensions to add. * blacklist should be a set() of extensions to blacklist.""" args = dict( lazy_ol=False, # If a numbered list starts at e.g. 4, show the <ol> there extensions=[ "fenced_code", # GitHub style code blocks "tables", # http://michelf.ca/projects/php-markdown/extra/#table "smart_strong", # Handles double__underscore better. "codehilite", # Code highlighting with Pygment! "nl2br", # Line breaks inside a paragraph become <br> "sane_lists", # Make lists less surprising ], extension_configs={ "codehilite": { "linenums": False, } } ) if html_escape: args["safe_mode"] = "escape" # Additional extensions? if extensions is not None: for ext in extensions: args["extensions"].append(ext) if blacklist is not None: for ext in blacklist: args["extensions"].remove(str(ext)) return u'<div class="markdown">{}</div>'.format( markdown.markdown(body, **args) ) def send_email(to, subject, message, sender=None, reply_to=None): """Send an e-mail out.""" if sender is None: sender = Config.mail.sender if type(to) != list: to = [to] logger.info("Send email to {}".format(to)) if Config.mail.method == "smtp": # Send mail with SMTP. for email in to: msg = MIMEMultipart("alternative") msg.set_charset("utf-8") msg["Subject"] = subject msg["From"] = sender msg["To"] = email if reply_to is not None: msg["Reply-To"] = reply_to text = MIMEText(message, "plain", "utf-8") msg.attach(text) # Send the e-mail. try: server = smtplib.SMTP(Config.mail.server, Config.mail.port) server.sendmail(sender, [email], msg.as_string()) except: pass def handle_exception(error): """Send an e-mail to the site admin when an exception occurs.""" if current_app.config.get("DEBUG"): print traceback.format_exc() raise import rophako.jsondb as JsonDB # Don't spam too many e-mails in a short time frame. cache = JsonDB.get_cache("exception_catcher") if cache: last_exception = int(cache) if int(time.time()) - last_exception < 120: # Only one e-mail per 2 minutes, minimum logger.error("RAPID EXCEPTIONS, DROPPING") return JsonDB.set_cache("exception_catcher", int(time.time())) username = "anonymous" try: if hasattr(g, "info") and "session" in g.info and "username" in g.info["session"]: username = g.info["session"]["username"] except: pass # Get the timestamp. timestamp = time.ctime(time.time()) # Exception's traceback. error = str(error.__class__.__name__) + ": " + str(error) stacktrace = error + "\n\n" \ + "==== Start Traceback ====\n" \ + traceback.format_exc() \ + "==== End Traceback ====\n\n" \ + "Request Information\n" \ + "-------------------\n" \ + "Address: " + remote_addr() + "\n" \ + "User Agent: " + request.user_agent.string + "\n" \ + "Referrer: " + request.referrer # Construct the subject and message subject = "Internal Server Error on {} - {} - {}".format( Config.site.site_name, username, timestamp, ) message = "{} has experienced an exception on the route: {}".format( username, request.path, ) message += "\n\n" + stacktrace # Send the e-mail. send_email( to=Config.site.notify_address, subject=subject, message=message, ) def generate_csrf_token(): """Generator for CSRF tokens.""" if "_csrf" not in session: session["_csrf"] = str(uuid.uuid4()) return session["_csrf"] def include(endpoint, *args, **kwargs): """Include another sub-page inside a template.""" # The 'endpoint' should be in the format 'module.function', i.e. 'blog.index'. module, function = endpoint.split(".") # Dynamically import the module and call its function. m = importlib.import_module("rophako.modules.{}".format(module)) html = getattr(m, function)(*args, **kwargs) return html def remote_addr(): """Retrieve the end user's remote IP address.""" # TODO: eventually support configurations with X-Forwarded-For, but for # now at least we're centralizing this in one spot. return request.remote_addr def server_name(): """Get the server's hostname.""" urlparts = list(urlparse.urlparse(request.url_root)) return urlparts[1] def pretty_time(time_format, unix): """Pretty-print a time stamp.""" date = datetime.datetime.fromtimestamp(unix) return date.strftime(time_format) def sanitize_name(name): """Sanitize a name that may be used in the filesystem. Only allows numbers, letters, and some symbols.""" return re.sub(r'[^A-Za-z0-9 .\-_]+', '', name)