From b36f9c05071ea549ed59e703270fcf223b60df03 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Sun, 9 Apr 2017 09:02:09 +0200 Subject: Major rewrite to use jab/oauth. Highlights: - Uses the oauth branch of jab. - Changed design to use bootstrap. - Some minor changes to functionality in file uploading and listing. - API is currently disabled and incomplete. --- fbin/__init__.py | 39 +++ fbin/api.py | 62 ++++ fbin/db.py | 121 +++++++ fbin/fbin.py | 353 +++++++++++++++++++++ fbin/login.py | 92 ++++++ fbin/monkey.py | 25 ++ fbin/static/css/bootstrap.min.css | 6 + fbin/static/css/style.css | 18 ++ fbin/static/fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes fbin/static/fonts/glyphicons-halflings-regular.svg | 288 +++++++++++++++++ fbin/static/fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../static/fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes fbin/static/img/no-thumbnail.png | Bin 0 -> 1546 bytes fbin/static/js/bootstrap-filestyle.min.js | 1 + fbin/static/js/bootstrap.min.js | 7 + fbin/static/js/jquery.lazy.min.js | 2 + fbin/static/js/jquery.min.js | 4 + fbin/templates/base.html | 70 ++++ fbin/templates/file-modals.html | 59 ++++ fbin/templates/files.html | 73 +++++ fbin/templates/help.html | 30 ++ fbin/templates/images.html | 29 ++ fbin/templates/modal.html | 17 + fbin/templates/upload.html | 25 ++ fbin/templates/uploaded.html | 14 + 26 files changed, 1335 insertions(+) create mode 100644 fbin/__init__.py create mode 100644 fbin/api.py create mode 100644 fbin/db.py create mode 100755 fbin/fbin.py create mode 100644 fbin/login.py create mode 100644 fbin/monkey.py create mode 100644 fbin/static/css/bootstrap.min.css create mode 100644 fbin/static/css/style.css create mode 100644 fbin/static/fonts/glyphicons-halflings-regular.eot create mode 100644 fbin/static/fonts/glyphicons-halflings-regular.svg create mode 100644 fbin/static/fonts/glyphicons-halflings-regular.ttf create mode 100644 fbin/static/fonts/glyphicons-halflings-regular.woff create mode 100644 fbin/static/fonts/glyphicons-halflings-regular.woff2 create mode 100644 fbin/static/img/no-thumbnail.png create mode 100644 fbin/static/js/bootstrap-filestyle.min.js create mode 100644 fbin/static/js/bootstrap.min.js create mode 100644 fbin/static/js/jquery.lazy.min.js create mode 100644 fbin/static/js/jquery.min.js create mode 100644 fbin/templates/base.html create mode 100644 fbin/templates/file-modals.html create mode 100644 fbin/templates/files.html create mode 100644 fbin/templates/help.html create mode 100644 fbin/templates/images.html create mode 100644 fbin/templates/modal.html create mode 100644 fbin/templates/upload.html create mode 100644 fbin/templates/uploaded.html (limited to 'fbin') diff --git a/fbin/__init__.py b/fbin/__init__.py new file mode 100644 index 0000000..bdeaa5d --- /dev/null +++ b/fbin/__init__.py @@ -0,0 +1,39 @@ +from flask import Flask, url_for, Markup, request +from flask_login import current_user +from werkzeug.routing import BaseConverter + +app = Flask(__name__) +app.config.from_pyfile('fbin.cfg') + +# Set up some custom converters. These are needed for file URLs to be properly parsed. + +class HashConverter(BaseConverter): + regex = r'\w+' + +class ExtensionConverter(BaseConverter): + regex = r'\.\w+' + +app.url_map.converters['hash'] = HashConverter +app.url_map.converters['ext'] = ExtensionConverter + +@app.context_processor +def context_processors(): + def nav_html(view, name=None): + url = url_for(view) + if not name: + name = view.rsplit('.', 1)[-1].replace('_', ' ').capitalize() + if view == '.logout': + name += ' [{}]'.format(current_user.username) + return Markup('{}'.format(' class="active"' if url == request.path else '', url, name)) + return { + 'nav_html': nav_html, + } + +with app.app_context(): + # TODO: Enable API when done + from .fbin import app as fbin + #from .api import app as api + from .login import login_manager + app.register_blueprint(fbin) + #app.register_blueprint(api, url_prefix = '/api') + login_manager.init_app(app) diff --git a/fbin/api.py b/fbin/api.py new file mode 100644 index 0000000..e652019 --- /dev/null +++ b/fbin/api.py @@ -0,0 +1,62 @@ +import functools + +from flask import Blueprint, current_app, request, jsonify +from flask.views import MethodView +from flask_login import current_user + +from . import db +# FIXME +from .fbin import get_file + +app = Blueprint('api', __name__) + +# TODO: Implement this stuff. + +def makejson(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + r = f(*args, **kwargs) + if isinstance(r, dict): + r = jsonify(r) + return r + return wrapper + +def api_login_required(f): + def wrapper(*args, **kwargs): + if not current_user.is_authenticated: + return { + 'status': False, + 'message': 'Not authenticated' + } + return f(*args, **kwargs) + return wrapper + +class FileAPI(MethodView): + decorators = [api_login_required, makejson] + + def put(self, hash): + f = get_file(hash, user_id = current_user.get_id()) + if not f: + return { + 'status': False, + 'message': 'File not found' + } + filename = request.form.get('filename') + if not filename: + return { + 'status': False, + 'message': 'Empty or missing filename', + } + with db.session_scope() as sess: + f.filename = filename + sess.add(f) + return { + 'status': True, + } + + def delete(self, hash): + pass + +file_api_view = FileAPI.as_view('file_api') +app.add_url_rule('/file/', view_func = file_api_view, methods = ['PUT', 'DELETE']) + diff --git a/fbin/db.py b/fbin/db.py new file mode 100644 index 0000000..dfe3b17 --- /dev/null +++ b/fbin/db.py @@ -0,0 +1,121 @@ +from contextlib import contextmanager +import mimetypes +import os + +from flask import current_app +from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Index, ForeignKey, Boolean +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relation, backref +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.exc import IntegrityError +from sqlalchemy.sql import and_ + +engine = create_engine(current_app.config['DB_URI']) + +Base = declarative_base(bind = engine) + +class User(Base): + __tablename__ = 'users' + + id = Column(Integer, primary_key = True) + username = Column(String, unique = True, index = True) + jab_id = Column(String(24), unique = True, index = True) + files = relation('File', backref = 'user', order_by = 'File.date.desc()') + + def __init__(self, username, jab_id): + self.username = username + self.jab_id = jab_id + +class UserSession(Base): + __tablename__ = 'sessions' + + id = Column(Integer, primary_key = True) + user_id = Column(Integer, ForeignKey('users.id'), index = True) + access_token = Column(String) + refresh_token = Column(String) + updated = Column(DateTime) + + def __init__(self, user_id, access_token, refresh_token): + self.user_id = user_id + self.access_token = access_token + self.refresh_token = refresh_token + +class File(Base): + __tablename__ = 'files' + + id = Column(Integer, primary_key = True) + hash = Column(String, unique = True, index = True) + filename = Column(String) + date = Column(DateTime) + user_id = Column(Integer, ForeignKey('users.id'), nullable = True) + ip = Column(String) + accessed = Column(DateTime) + + def __init__(self, hash, filename, date, user_id = None, ip = None): + self.hash = hash + self.filename = filename + self.date = date + self.user_id = user_id + self.ip = ip + + @staticmethod + def pretty_size(size): + if size is None: + return 'N/A' + suffixes = (('TiB', 2**40), ('GiB', 2**30), ('MiB', 2**20), ('KiB', 2**10)) + for suf, threshold in suffixes: + if size >= threshold: + return '{:.2f} {}'.format(size / threshold, suf) + else: + continue + return '{} B'.format(size) + + def get_path(self): + return os.path.join(current_app.config['FILE_DIRECTORY'], self.hash + os.path.splitext(self.filename)[1]) + + def get_thumb_path(self): + return os.path.join(current_app.config['THUMB_DIRECTORY'], self.hash + '.jpg') + + def get_size(self): + try: + return os.path.getsize(self.get_path()) + except OSError: + return None + + @property + def formatted_size(self): + return self.pretty_size(self.get_size()) + + @property + def formatted_date(self): + return self.date.strftime('%Y-%m-%d %H:%M:%S UTC') + + def get_mime_type(self): + return mimetypes.guess_type(self.filename, strict = False)[0] or 'application/octet-stream' + + def is_image(self): + return self.get_mime_type().startswith('image') + + @property + def ext(self): + return os.path.splitext(self.filename)[1] + + @property + def exists(self): + return os.path.exists(self.get_path()) + +Base.metadata.create_all() +Session = sessionmaker(bind = engine, autoflush = True, autocommit = False) + +@contextmanager +def session_scope(): + session = Session() + try: + session.expire_on_commit = False + yield session + session.commit() + except: + session.rollback() + raise + finally: + session.close() diff --git a/fbin/fbin.py b/fbin/fbin.py new file mode 100755 index 0000000..42c371f --- /dev/null +++ b/fbin/fbin.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python + +import base64 +import cgi +import datetime +import hashlib +import io +import json +import mimetypes +import os +import random +import subprocess +import tempfile +import urllib +from urllib.parse import urlencode, urljoin + +from flask import Blueprint, redirect, current_app, url_for, request, render_template, session, flash, send_file, abort, jsonify, Markup +from flask_login import login_user, logout_user, current_user, login_required +import jwt +from PIL import Image +import requests +from werkzeug.utils import secure_filename + +from . import db +from .monkey import patch as monkey_patch +from .login import login_manager, load_user + +monkey_patch() + +base62_alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + +if not os.path.isdir(current_app.config['FILE_DIRECTORY']): + os.mkdir(current_app.config['FILE_DIRECTORY']) + +if not os.path.isdir(current_app.config['THUMB_DIRECTORY']): + os.mkdir(current_app.config['THUMB_DIRECTORY']) + +try: + # Throws OSError if mogrify doesn't exist. + subprocess.call(['mogrify', '-quiet']) +except OSError: + has_mogrify = False +else: + has_mogrify = True + +def get_or_create_user(username, jab_id): + with db.session_scope() as sess: + try: + return sess.query(db.User).filter(db.User.jab_id == jab_id).one() + except db.NoResultFound: + try: + user = db.User(username, jab_id) + sess.add(user) + sess.commit() + sess.refresh(user) + return user + except db.IntegrityError: + return None + +def add_file(path, filename, user = None, ip = None): + file_hash = ''.join(random.choice(base62_alphabet) for x in range(5)) + new_path = os.path.join(current_app.config['FILE_DIRECTORY'], file_hash + os.path.splitext(filename)[1]) + os.rename(path, new_path) + if current_app.config.get('DESTINATION_MODE'): + os.chmod(new_path, current_app.config.get('DESTINATION_MODE')) + with db.session_scope() as sess: + f = db.File(file_hash, filename, datetime.datetime.utcnow(), user.id if user else None, ip) + sess.add(f) + sess.commit() + sess.refresh(f) + return f + +def get_file(file_hash, user_id=None, update_accessed=False): + with db.session_scope() as sess: + try: + f = sess.query(db.File).filter(db.File.hash == file_hash) + if user_id: + f = f.filter(db.File.user_id == user_id) + f = f.one() + except db.NoResultFound: + return None + if update_accessed: + f.accessed = datetime.datetime.utcnow() + sess.add(f) + sess.commit() + # Refresh after field update. + sess.refresh(f) + return f + +def get_files(user): + with db.session_scope() as sess: + try: + sess.add(user) + files = user.files + except db.NoResultFound: + return [] + return files + +def delete_file(file): + with db.session_scope() as sess: + sess.delete(file) + sess.commit() + filename = file.get_path() + if os.path.exists(filename): + os.unlink(filename) + thumbfile = file.get_thumb_path() + if os.path.exists(thumbfile): + os.unlink(thumbfile) + +app = Blueprint('fbin', __name__) + +@app.route('/') +def index(): + return redirect(url_for('.upload')) + +@app.route('/u') +@app.route('/upload', methods = ['GET', 'POST']) +def upload(): + context = { + 'title': 'Upload', + } + if request.method == 'GET': + return render_template('upload.html', **context) + if not current_user.is_authenticated and not current_app.config.get('ALLOW_ANONYMOUS_UPLOADS'): + abort(403) + uploaded_file = request.files.get('file') + if not uploaded_file or not uploaded_file.filename: + flash('No valid file or filename was provided.', 'warning') + return render_template('upload.html', **context) + if hasattr(uploaded_file.stream, 'file'): + temp = None + temp_path = uploaded_file.stream.name + else: + temp = tempfile.NamedTemporaryFile(prefix = 'upload_', dir = current_app.config['FILE_DIRECTORY'], delete = False) + uploaded_file.save(temp.file) + temp_path = temp.name + new_file = add_file(temp_path, uploaded_file.filename, current_user.user if current_user.is_authenticated else None, request.remote_addr) + + mime = new_file.get_mime_type() + # TODO: Apparently TIFF also supports EXIF, test this. + if has_mogrify and mime == 'image/jpeg': + # NOTE: PIL doesn't support lossless rotation, so we call mogrify to do this. + # NOTE: This changes the file, so the file_hash applies to the ORIGINAL file contents only. + # NOTE: The file hash is only used to detect duplicates when uploading, so this should not be a problem. + subprocess.call(['mogrify', '-auto-orient', new_file.get_path()]) + + if bool(request.form.get('api')): + return 'OK {hash}'.format(hash = new_file.hash) + else: + context = { + 'file': new_file, + } + return redirect(url_for('.uploaded', hash = new_file.hash)) + +@app.route('/uploaded/') +def uploaded(hash): + f = get_file(hash, update_accessed = False) + if not f: + abort(404) + if f.user_id and (not current_user.is_authenticated or f.user_id != current_user.get_user_id()): + abort(404) + context = { + 'title': 'Uploaded', + 'subtitle': f.filename, + 'file': f, + } + return render_template('uploaded.html', **context) + +@app.route('/f/') +@app.route('/f/') +@app.route('/f//') +@app.route('/file/', endpoint = 'file') +@app.route('/file/', endpoint = 'file') +@app.route('/file//', endpoint = 'file') +def _file(hash, ext=None, filename=None): + f = get_file(hash) + if not f: + abort(404) + return send_file(f.get_path()) + +@app.route('/l') +@app.route('/login') +def login(): + session['oauth_state'] = base64.urlsafe_b64encode(os.urandom(30)).decode() + return redirect(urljoin(current_app.config['OAUTH_URL'], 'authorize') + '?' + urlencode({ + 'response_type': 'code', + 'client_id': current_app.config['OAUTH_CLIENT_ID'], + 'redirect_uri': urljoin(request.host_url, url_for('.auth')), + 'state': session['oauth_state'], + })) + +@app.route('/account') +def account(): + return redirect(current_app.config['ACCOUNT_URL']) + +@app.route('/o') +@app.route('/logout') +def logout(): + if not current_user.is_authenticated: + return redirect(url_for('.index')) + session_id = int(current_user.get_id().split(':', 1)[-1]) + with db.session_scope() as s: + try: + s.query(db.UserSession).filter_by(id = session_id).delete() + except: + raise + logout_user() + return redirect(url_for('.index')) + +@app.route('/auth') +def auth(): + if 'error' in request.args: + error = request.args['error'] + error_description = request.args.get('error_description') + msg = 'OAuth error: {}'.format(error) + if error_description: + msg += ' ({})'.format(error_description) + flash(msg, 'error') + return redirect(url_for('.index')) + session_state = session.pop('oauth_state', None) + state = request.args.get('state') + if session_state != state: + flash('Invalid OAuth state', 'error') + return redirect(url_for('.index')) + code = request.args.get('code') + if not code: + flash('Missing OAuth code', 'error') + return redirect(url_for('.index')) + rs = requests.Session() + response = rs.post(urljoin(current_app.config['OAUTH_URL'], 'token'), data = { + 'grant_type': 'authorization_code', + 'code': code, + 'client_id': current_app.config['OAUTH_CLIENT_ID'], + 'client_secret': current_app.config['OAUTH_CLIENT_SECRET'], + 'redirect_uri': urljoin(request.host_url, url_for('.auth')), + }) + token = response.json() + if 'error' in token: + msg = 'OAuth error: {}'.format(token['error']) + flash(msg, 'error') + return redirect(url_for('.index')) + try: + access_data = jwt.decode(token['access_token'], key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) + refresh_data = jwt.decode(token['refresh_token'], key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) + except jwt.InvalidTokenError as e: + flash('Failed to verify token: {!s}'.format(e), 'error') + return redirect(url_for('.index')) + response = rs.get(urljoin(current_app.config['OAUTH_URL'], '/api/user'), headers = {'Authorization': 'Bearer {}'.format(token['access_token'])}) + user = response.json() + user = get_or_create_user(user['username'], user['id']) + with db.session_scope() as s: + us = db.UserSession(user.id, token['access_token'], token['refresh_token']) + us.updated = datetime.datetime.utcnow() + s.add(us) + s.commit() + s.refresh(us) + user = load_user('{}:{}'.format(user.id, us.id)) + if not user: + flash('Failed to retrieve user instance.', 'error') + else: + login_user(user, remember = True) + return redirect(url_for('.index')) + +@app.route('/m') +@app.route('/files') +@login_required +def files(): + files = get_files(current_user.user) + context = { + 'title': 'Files', + 'files': files, + 'total_size': db.File.pretty_size(sum(f.get_size() for f in files if f.exists)), + } + return render_template('files.html', **context) + +@app.route('/files', methods = ['POST']) +@login_required +def file_edit(): + f = get_file(request.form.get('hash'), user_id = current_user.get_id(), update_accessed = False) + if not f: + flash('File not found.', 'error') + return redirect(url_for('.files')) + if 'filename' in request.form: + with db.session_scope() as sess: + old_path = f.get_path() + filename = request.form.get('filename', f.filename) + f.filename = filename + new_path = f.get_path() + # If extension changed, the local filename also changes. We could just store the file without the extension, + # but that would break the existing files, requiring a manual rename. + if old_path != new_path: + try: + if os.path.exists(new_path): + # This shouldn't happen unless we have two files with the same hash, which should be impossible. + raise RuntimeError() + else: + os.rename(old_path, new_path) + except: + flash(Markup('Internal rename failed; file may have become unreachable. ' + 'Please contact an admin and specify hash={}.'.format(f.hash)), 'error') + sess.add(f) + flash('Filename changed to "{}".'.format(f.filename), 'success') + elif 'delete' in request.form: + try: + delete_file(f) + except: + flash('Failed to delete file.', 'error') + else: + flash('File deleted.', 'success') + else: + flash('No action was performed.', 'warning') + return redirect(url_for('.files')) + +@app.route('/i') +@app.route('/images') +@login_required +def images(): + files = [f for f in get_files(current_user.user) if f.is_image()] + context = { + 'title': 'Images', + 'fullwidth': True, + 'files': files, + 'total_size': db.File.pretty_size(sum(f.get_size() for f in files if f.exists)), + } + return render_template('images.html', **context) + +@app.route('/t/') +@app.route('/thumb/') +def thumb(hash): + thumbfile = os.path.join(current_app.config['THUMB_DIRECTORY'], hash + '.jpg') + if not os.access(thumbfile, os.F_OK): + f = get_file(hash, update_accessed = False) + try: + im = Image.open(f.get_path()) + except IOError: + # We can't generate a thumbnail for this file, just say it doesn't exist. + abort(404) + # Check for valid JPEG modes. + if im.mode not in ('1', 'L', 'RGB', 'RGBA', 'RGBX', 'CMYK', 'YCbCr'): + im = im.convert('RGB') + im.thumbnail(current_app.config.get('THUMB_SIZE', (128, 128)), Image.ANTIALIAS) + im.save(thumbfile) + return send_file(thumbfile) + +@app.route('/h') +@app.route('/help') +def help(): + context = { + 'title': 'Help', + } + return render_template('help.html', **context) + +login_manager.login_view = '.login' diff --git a/fbin/login.py b/fbin/login.py new file mode 100644 index 0000000..7647e13 --- /dev/null +++ b/fbin/login.py @@ -0,0 +1,92 @@ +import datetime +import traceback +from urllib.parse import urljoin + +from flask import current_app, flash +from flask_login import LoginManager +import jwt +import requests + +from . import db + +login_manager = LoginManager() + +class User: + def __init__(self, user, user_session): + self.user = user + self.user_session = user_session + self.token = None + + def refresh_access_token(self): + response = requests.post(urljoin(current_app.config['OAUTH_URL'], 'token'), data = { + 'grant_type': 'refresh_token', + 'client_id': current_app.config['OAUTH_CLIENT_ID'], + 'client_secret': current_app.config['OAUTH_CLIENT_SECRET'], + 'refresh_token': self.user_session.refresh_token, + }) + if response.status_code != 200: + flash('Failed to refresh authentication token (API call returned {} {})'.format(response.status_code, response.reason), 'error') + return + token = response.json() + try: + access_data = jwt.decode(token['access_token'], key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) + refresh_data = jwt.decode(token['refresh_token'], key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) + except jwt.InvalidTokenError as e: + traceback.print_exc() + flash('Failed to refresh authentication token (verification failed)', 'error') + return + with db.session_scope() as sess: + self.user_session.access_token = token['access_token'] + self.user_session.refresh_token = token['refresh_token'] + self.user_session.updated = datetime.datetime.utcnow() + sess.add(self.user_session) + return True + + @property + def is_authenticated(self): + if self.user is None: + return False + if self.token: + return True + try: + self.token = jwt.decode(self.user_session.access_token, key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) + except jwt.ExpiredSignatureError: + try: + if not self.refresh_access_token(): + return False + except: + traceback.print_exc() + flash('Failed to refresh authentication token (unhandled error; contact an admin)', 'error') + return False + except jwt.InvalidTokenError: + return False + return True + + @property + def is_active(self): + return True + + @property + def is_anonymous(self): + return False + + def get_id(self): + return '{}:{}'.format(self.user.id, self.user_session.id) + + def get_user_id(self): + return self.user.id if self.is_authenticated else None + + @property + def username(self): + return self.user.username + +@login_manager.user_loader +def load_user(user_id): + user_id, session_id = map(int, user_id.split(':', 1)) + try: + with db.session_scope() as sess: + user, user_session = sess.query(db.User, db.UserSession).join(db.UserSession).filter(db.User.id == user_id, db.UserSession.id == session_id).one() + return User(user, user_session) + except: + traceback.print_exc() + return None diff --git a/fbin/monkey.py b/fbin/monkey.py new file mode 100644 index 0000000..c6458ad --- /dev/null +++ b/fbin/monkey.py @@ -0,0 +1,25 @@ +import tempfile + +from flask import current_app +import werkzeug.formparser +import werkzeug.wrappers + +def werkzeug_patch(): + global werkzeug_orig_stream_factory + + werkzeug_orig_stream_factory = werkzeug.formparser.default_stream_factory + + def custom_stream_factory(total_content_length, filename, content_type, content_length=None): + if total_content_length > 1024 * 500: + return tempfile.NamedTemporaryFile('wb+', prefix = 'upload_', dir = current_app.config['FILE_DIRECTORY'], delete = True) + return werkzeug_orig_stream_factory(total_content_length, filename, content_type, content_length) + + werkzeug.formparser.default_stream_factory = custom_stream_factory + werkzeug.wrappers.default_stream_factory = custom_stream_factory + +def werkzeug_reset(): + werkzeug.formparser.default_stream_factory = werkzeug_orig_stream_factory + werkzeug.wrappers.default_stream_factory = werkzeug_orig_stream_factory + +def patch(): + werkzeug_patch() diff --git a/fbin/static/css/bootstrap.min.css b/fbin/static/css/bootstrap.min.css new file mode 100644 index 0000000..ed3905e --- /dev/null +++ b/fbin/static/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition: