New clean layout to be pip installable
@ -3,22 +3,38 @@
|
||||
# import default settings value from src/default_settings.py
|
||||
# you can refer to this file if you forgot what
|
||||
# settings is for and what it is set to by default
|
||||
# DO NOT ALTER THIS LINE
|
||||
from src.default_settings import *
|
||||
# You probably do not want to alter this line
|
||||
from zerobin.default_settings import *
|
||||
|
||||
# debug will get you error message and auto reload
|
||||
# don't set this to True in production
|
||||
DEBUG = False
|
||||
DEBUG = True
|
||||
|
||||
# Should the application serve static files on it's own ?
|
||||
# IF yes, set the absolute path to the static files.
|
||||
# If no, set it to None
|
||||
# In dev this is handy, in prod you probably want the HTTP servers
|
||||
# to serve it, but it's OK for small traffic to set it to True in prod too.
|
||||
STATIC_FILES_ROOT = os.path.join(ROOT_DIR, 'static')
|
||||
|
||||
# absolute path where the paste files should be store
|
||||
# default in projectdirectory/static/content/
|
||||
# use "/" even under Windows
|
||||
PASTE_FILES_ROOT = os.path.join(STATIC_FILES_ROOT, 'content')
|
||||
PASTE_FILES_ROOT = os.path.join(ROOT_DIR, 'static', 'content')
|
||||
|
||||
# a tuple of absolute paths of directory where to look the template for
|
||||
# the first one will be the first to be looked into
|
||||
# if you want to override a template, create a new dir, write the
|
||||
# template with the same name as the one you want to override in it
|
||||
# then add the dir path at the top of this tuple
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(ROOT_DIR, 'views'),
|
||||
)
|
||||
|
||||
# Port and host the embeded python server should be using
|
||||
# You can also specify them using the --host and --port script options
|
||||
# which have priority on these settings
|
||||
HOST = "127.0.0.1"
|
||||
HOST = "0.0.0.0"
|
||||
PORT = "8000"
|
||||
|
||||
# User and group the server should run as. Set to None if it should be the
|
||||
@ -26,23 +42,16 @@ PORT = "8000"
|
||||
USER = None
|
||||
GROUP = None
|
||||
|
||||
# limit size of pasted text in bytes. Be carefull allowing too much
|
||||
# size can slow down user's browser
|
||||
MAX_SIZE = 1024 * 500
|
||||
MAX_SIZE_KB = int(math.ceil(MAX_SIZE / 1024.0))
|
||||
|
||||
# Names/links to insert in the menu bar.
|
||||
# Any link with "mailto:" will be escaped to prevent spam
|
||||
MENU = (
|
||||
('Home', '/'), # internal link
|
||||
('Home', '/'), # internal link. First link will be highlited
|
||||
('Download 0bin', 'https://github.com/sametmax/0bin'), # external link
|
||||
('Contact', 'mailto:your@email.com') # email
|
||||
)
|
||||
|
||||
# this import a file named settings_local.py if it exists
|
||||
# you may want to create such a file to have different settings
|
||||
# on each machine
|
||||
try:
|
||||
from settings_local import *
|
||||
except ImportError:
|
||||
pass
|
||||
# limit size of pasted text in bytes. Be carefull allowing too much size can slow down user's
|
||||
# browser
|
||||
MAX_SIZE = 1024 * 500
|
||||
MAX_SIZE_KB = int(math.ceil(MAX_SIZE / 1024.0))
|
||||
|
@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
49
src/utils.py
@ -1,49 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
import os
|
||||
import glob
|
||||
import tempfile
|
||||
|
||||
|
||||
try:
|
||||
from privilege import drop_privileges_permanently, coerce_user, coerce_group
|
||||
except (AttributeError):
|
||||
pass # privilege does't work on several plateform
|
||||
|
||||
|
||||
def drop_privileges(user=None, group=None, wait=5):
|
||||
"""
|
||||
Try to set the process user and group to another one.
|
||||
If no group is provided, it's set to the same as the user.
|
||||
You can wait for a certain time before doing so.
|
||||
"""
|
||||
if wait:
|
||||
time.sleep(wait)
|
||||
if user:
|
||||
group = group or user
|
||||
try:
|
||||
user = coerce_user(user)
|
||||
group = coerce_group(group)
|
||||
|
||||
lock_files = glob.glob(os.path.join(tempfile.gettempdir(),
|
||||
'bottle.*.lock'))
|
||||
for lock_file in lock_files:
|
||||
os.chown(lock_file, user, group)
|
||||
|
||||
drop_privileges_permanently(user, group, ())
|
||||
except Exception:
|
||||
print "Failed to drop privileges. Running with current user."
|
||||
|
||||
|
||||
def dmerge(*args):
|
||||
"""
|
||||
return new directionay being the sum of all merged dictionaries passed as arguments
|
||||
"""
|
||||
|
||||
dictionary = {}
|
||||
|
||||
for arg in args:
|
||||
dictionary.update(arg)
|
||||
|
||||
return dictionary
|
131
start.py
@ -2,133 +2,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: ai ts=4 sts=4 et sw=4
|
||||
|
||||
"""
|
||||
Main script including controller, rooting, dependancy management, and
|
||||
server run.
|
||||
"""
|
||||
from zerobin.routes import main
|
||||
|
||||
import sys
|
||||
import os
|
||||
import thread
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# add project dir and libs dir to the PYTHON PATH to ensure they are
|
||||
# importable
|
||||
import settings
|
||||
sys.path.insert(0, os.path.dirname(settings.ROOT_DIR))
|
||||
sys.path.append(os.path.join(settings.ROOT_DIR, 'libs'))
|
||||
|
||||
import bottle
|
||||
from bottle import (Bottle, run, static_file, view, request)
|
||||
|
||||
import clize
|
||||
|
||||
|
||||
from src.paste import Paste
|
||||
from src.utils import drop_privileges, dmerge
|
||||
|
||||
|
||||
app = Bottle()
|
||||
|
||||
global_vars = {
|
||||
'settings': settings
|
||||
}
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@view('home')
|
||||
def index():
|
||||
return global_vars
|
||||
|
||||
|
||||
@app.route('/paste/create', method='POST')
|
||||
def create_paste():
|
||||
|
||||
try:
|
||||
content = unicode(request.forms.get('content', ''), 'utf8')
|
||||
except UnicodeDecodeError:
|
||||
return {'status': 'error',
|
||||
'message': u"Encoding error: the paste couldn't be saved."}
|
||||
|
||||
if '{"iv":' not in content: # reject silently non encrypted content
|
||||
return ''
|
||||
|
||||
if content:
|
||||
# check size of the paste. if more than settings return error without saving paste.
|
||||
# prevent from unusual use of the system.
|
||||
# need to be improved
|
||||
if len(content) < settings.MAX_SIZE:
|
||||
expiration = request.forms.get('expiration', u'burn_after_reading')
|
||||
paste = Paste(expiration=expiration, content=content)
|
||||
paste.save()
|
||||
return {'status': 'ok',
|
||||
'paste': paste.uuid}
|
||||
|
||||
return {'status': 'error',
|
||||
'message': u"Serveur error: the paste couldn't be saved. Please try later."}
|
||||
|
||||
|
||||
@app.route('/paste/:paste_id')
|
||||
@view('paste')
|
||||
def display_paste(paste_id):
|
||||
|
||||
now = datetime.now()
|
||||
keep_alive = False
|
||||
try:
|
||||
paste = Paste.load(paste_id)
|
||||
# Delete the paste if it expired:
|
||||
if 'burn_after_reading' in str(paste.expiration):
|
||||
# burn_after_reading contains the paste creation date
|
||||
# if this read appends 10 seconds after the creation date
|
||||
# we don't delete the paste because it means it's the redirection
|
||||
# to the paste that happens during the paste creation
|
||||
try:
|
||||
keep_alive = paste.expiration.split('#')[1]
|
||||
keep_alive = datetime.strptime(keep_alive, '%Y-%m-%d %H:%M:%S.%f')
|
||||
keep_alive = now < keep_alive + timedelta(seconds=10)
|
||||
except IndexError:
|
||||
keep_alive = False
|
||||
if not keep_alive:
|
||||
paste.delete()
|
||||
|
||||
elif paste.expiration < now:
|
||||
paste.delete()
|
||||
raise ValueError()
|
||||
|
||||
except (TypeError, ValueError):
|
||||
#abort(404, u"This paste doesn't exist or has expired")
|
||||
return error404(ValueError)
|
||||
|
||||
context = {'paste': paste, 'keep_alive': keep_alive}
|
||||
return dmerge(context, global_vars)
|
||||
|
||||
|
||||
@app.error(404)
|
||||
@view('404')
|
||||
def error404(code):
|
||||
return global_vars
|
||||
|
||||
|
||||
@clize.clize
|
||||
def runserver(host=settings.HOST, port=settings.PORT, debug=settings.DEBUG,
|
||||
serve_static=settings.DEBUG, user=settings.USER,
|
||||
group=settings.GROUP):
|
||||
|
||||
if serve_static:
|
||||
@app.route('/static/<filename:path>')
|
||||
def server_static(filename):
|
||||
return static_file(filename, root=settings.STATIC_FILES_ROOT)
|
||||
|
||||
thread.start_new_thread(drop_privileges, (user, group))
|
||||
|
||||
if debug:
|
||||
bottle.debug(True)
|
||||
run(app, host=host, port=port, reloader=True, server="cherrypy")
|
||||
else:
|
||||
run(app, host=host, port=port, server="cherrypy")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
clize.run(runserver)
|
||||
main()
|
@ -2,21 +2,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: ai ts=4 sts=4 et sw=4
|
||||
|
||||
|
||||
######## NOT SETTINGS, JUST BOILER PLATE ##############
|
||||
import os
|
||||
import math
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STATIC_FILES_ROOT = os.path.join(ROOT_DIR, 'static')
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LIBS_DIR = os.path.join(os.path.dirname(ROOT_DIR), 'libs')
|
||||
|
||||
######## END OF BOILER PLATE ##############
|
||||
|
||||
# debug will get you error message and auto reload
|
||||
# don't set this to True in production
|
||||
DEBUG = False
|
||||
|
||||
# Should the application serve static files on it's own ?
|
||||
# IF yes, set the absolute path to the static files.
|
||||
# If no, set it to None
|
||||
# In dev this is handy, in prod you probably want the HTTP servers
|
||||
# to serve it, but it's OK for small traffic to set it to True in prod too.
|
||||
STATIC_FILES_ROOT = os.path.join(ROOT_DIR, 'static')
|
||||
|
||||
# absolute path where the paste files should be store
|
||||
# default in projectdirectory/static/content/
|
||||
# use "/" even under Windows
|
||||
PASTE_FILES_ROOT = os.path.join(STATIC_FILES_ROOT, 'content')
|
||||
|
||||
# a tuple of absolute paths of directory where to look the template for
|
||||
# the first one will be the first to be looked into
|
||||
# if you want to override a template, create a new dir, write the
|
||||
# template with the same name as the one you want to override in it
|
||||
# then add the dir path at the top of this tuple
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(ROOT_DIR, 'views'),
|
||||
)
|
||||
|
||||
# Port and host the embeded python server should be using
|
||||
# You can also specify them using the --host and --port script options
|
||||
# which have priority on these settings
|
@ -2,11 +2,10 @@
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import settings
|
||||
from utils import settings
|
||||
|
||||
|
||||
class Paste(object):
|
||||
@ -25,12 +24,10 @@ class Paste(object):
|
||||
|
||||
|
||||
def __init__(self, uuid=None, content=None,
|
||||
expiration=None,
|
||||
comments=None):
|
||||
expiration=None):
|
||||
|
||||
self.content = content
|
||||
self.expiration = expiration
|
||||
self.comments = comments
|
||||
|
||||
if isinstance(self.content, unicode):
|
||||
self.content = self.content.encode('utf8')
|
||||
@ -95,7 +92,6 @@ class Paste(object):
|
||||
uuid = os.path.basename(path)
|
||||
expiration = paste.next().strip()
|
||||
content = paste.next().strip()
|
||||
comments = paste.read()[:-1] # remove the last coma
|
||||
if "burn_after_reading" not in str(expiration):
|
||||
expiration = datetime.strptime(expiration, '%Y-%m-%d %H:%M:%S.%f')
|
||||
|
||||
@ -104,8 +100,7 @@ class Paste(object):
|
||||
except (IOError, OSError):
|
||||
raise ValueError(u'Can not open paste from file %s' % path)
|
||||
|
||||
return Paste(uuid=uuid, comments=comments,
|
||||
expiration=expiration, content=content)
|
||||
return Paste(uuid=uuid, expiration=expiration, content=content)
|
||||
|
||||
|
||||
@classmethod
|
||||
@ -120,9 +115,6 @@ class Paste(object):
|
||||
def save(self):
|
||||
"""
|
||||
Save the content of this paste to a file.
|
||||
|
||||
If comments are passed, they are expected to be serialized
|
||||
already.
|
||||
"""
|
||||
head, tail = self.uuid[:2], self.uuid[2:4]
|
||||
|
||||
@ -157,8 +149,6 @@ class Paste(object):
|
||||
with open(self.path, 'w') as f:
|
||||
f.write(unicode(self.expiration) + '\n')
|
||||
f.write(self.content + '\n')
|
||||
if self.comments:
|
||||
f.write(self.comments)
|
||||
|
||||
return self
|
||||
|
||||
@ -169,26 +159,3 @@ class Paste(object):
|
||||
"""
|
||||
os.remove(self.path)
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_comment_to(cls, uuid, **comment):
|
||||
"""
|
||||
Append a comment to the file of the paste with the given uuid.
|
||||
The comment is serialized to json, and a comma is added at the
|
||||
end of it. Then the result is appended to the paste file.
|
||||
This way we can add sequencially all comments to the file by just
|
||||
appending to it, and then extracting the comment by selecting
|
||||
this big blob of text, adding [] around it and use it as a json list
|
||||
with no extra processing.
|
||||
"""
|
||||
with open(cls.get_path(uuid), 'a') as f:
|
||||
f.write(json.dumps(comment) + u',\n')
|
||||
|
||||
|
||||
def add_comment(self, **comment):
|
||||
"""
|
||||
Append a comment to the file of this paste.
|
||||
|
||||
Use add_comment_to()
|
||||
"""
|
||||
self.add_comment_to(self.uuid, **comment)
|
141
zerobin/routes.py
Normal file
@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: ai ts=4 sts=4 et sw=4
|
||||
|
||||
"""
|
||||
Main script including controller, rooting, dependancy management, and
|
||||
server run.
|
||||
"""
|
||||
|
||||
import thread
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# add project dir and libs dir to the PYTHON PATH to ensure they are
|
||||
# importable
|
||||
from utils import settings
|
||||
|
||||
import bottle
|
||||
from bottle import (Bottle, run, static_file, view, request)
|
||||
|
||||
import clize
|
||||
|
||||
from paste import Paste
|
||||
from utils import drop_privileges, dmerge
|
||||
|
||||
|
||||
app = Bottle()
|
||||
GLOBAL_CONTEXT = {
|
||||
'settings': settings
|
||||
}
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@view('home')
|
||||
def index():
|
||||
return GLOBAL_CONTEXT
|
||||
|
||||
|
||||
@app.route('/paste/create', method='POST')
|
||||
def create_paste():
|
||||
|
||||
try:
|
||||
content = unicode(request.forms.get('content', ''), 'utf8')
|
||||
except UnicodeDecodeError:
|
||||
return {'status': 'error',
|
||||
'message': u"Encoding error: the paste couldn't be saved."}
|
||||
|
||||
if '{"iv":' not in content: # reject silently non encrypted content
|
||||
return ''
|
||||
|
||||
if content:
|
||||
# check size of the paste. if more than settings return error without saving paste.
|
||||
# prevent from unusual use of the system.
|
||||
# need to be improved
|
||||
if len(content) < settings.MAX_SIZE:
|
||||
expiration = request.forms.get('expiration', u'burn_after_reading')
|
||||
paste = Paste(expiration=expiration, content=content)
|
||||
paste.save()
|
||||
return {'status': 'ok',
|
||||
'paste': paste.uuid}
|
||||
|
||||
return {'status': 'error',
|
||||
'message': u"Serveur error: the paste couldn't be saved. Please try later."}
|
||||
|
||||
|
||||
@app.route('/paste/:paste_id')
|
||||
@view('paste')
|
||||
def display_paste(paste_id):
|
||||
|
||||
now = datetime.now()
|
||||
keep_alive = False
|
||||
try:
|
||||
paste = Paste.load(paste_id)
|
||||
# Delete the paste if it expired:
|
||||
if 'burn_after_reading' in str(paste.expiration):
|
||||
# burn_after_reading contains the paste creation date
|
||||
# if this read appends 10 seconds after the creation date
|
||||
# we don't delete the paste because it means it's the redirection
|
||||
# to the paste that happens during the paste creation
|
||||
try:
|
||||
keep_alive = paste.expiration.split('#')[1]
|
||||
keep_alive = datetime.strptime(keep_alive, '%Y-%m-%d %H:%M:%S.%f')
|
||||
keep_alive = now < keep_alive + timedelta(seconds=10)
|
||||
except IndexError:
|
||||
keep_alive = False
|
||||
if not keep_alive:
|
||||
paste.delete()
|
||||
|
||||
elif paste.expiration < now:
|
||||
paste.delete()
|
||||
raise ValueError()
|
||||
|
||||
except (TypeError, ValueError):
|
||||
#abort(404, u"This paste doesn't exist or has expired")
|
||||
return error404(ValueError)
|
||||
|
||||
context = {'paste': paste, 'keep_alive': keep_alive}
|
||||
return dmerge(context, GLOBAL_CONTEXT)
|
||||
|
||||
|
||||
@app.error(404)
|
||||
@view('404')
|
||||
def error404(code):
|
||||
return GLOBAL_CONTEXT
|
||||
|
||||
|
||||
@clize.clize
|
||||
def runserver(host='', port='', debug=None, serve_static='', user='',
|
||||
group='', settings_file=''):
|
||||
|
||||
# merge the settings
|
||||
if settings_file:
|
||||
settings.update_with_file(settings_file)
|
||||
|
||||
settings.HOST = host or settings.HOST
|
||||
settings.PORT = port or settings.PORT
|
||||
settings.DEBUG = debug if debug is not None else settings.DEBUG
|
||||
settings.STATIC_FILES_ROOT = serve_static or settings.STATIC_FILES_ROOT
|
||||
settings.USER = user or settings.USER
|
||||
settings.GROUP = group or settings.GROUP
|
||||
|
||||
# make sure the templates can be loaded
|
||||
for d in reversed(settings.TEMPLATE_DIRS):
|
||||
bottle.TEMPLATE_PATH.insert(0, d)
|
||||
|
||||
if serve_static:
|
||||
@app.route('/static/<filename:path>')
|
||||
def server_static(filename):
|
||||
return static_file(filename, root=settings.STATIC_FILES_ROOT)
|
||||
|
||||
thread.start_new_thread(drop_privileges, (settings.USER, settings.GROUP))
|
||||
|
||||
if settings.DEBUG:
|
||||
bottle.debug(True)
|
||||
run(app, host=settings.HOST, port=settings.PORT, reloader=True, server="cherrypy")
|
||||
else:
|
||||
run(app, host=settings.HOST, port=settings.PORT, server="cherrypy")
|
||||
|
||||
|
||||
def main():
|
||||
clize.run(runserver)
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 968 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
106
zerobin/utils.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
import os
|
||||
import glob
|
||||
import tempfile
|
||||
import sys
|
||||
|
||||
import default_settings
|
||||
sys.path.append(default_settings.LIBS_DIR)
|
||||
|
||||
try:
|
||||
from privilege import drop_privileges_permanently, coerce_user, coerce_group
|
||||
except (AttributeError):
|
||||
pass # privilege does't work on several plateform
|
||||
|
||||
|
||||
def drop_privileges(user=None, group=None, wait=5):
|
||||
"""
|
||||
Try to set the process user and group to another one.
|
||||
If no group is provided, it's set to the same as the user.
|
||||
You can wait for a certain time before doing so.
|
||||
"""
|
||||
if wait:
|
||||
time.sleep(wait)
|
||||
if user:
|
||||
group = group or user
|
||||
try:
|
||||
user = coerce_user(user)
|
||||
group = coerce_group(group)
|
||||
|
||||
lock_files = glob.glob(os.path.join(tempfile.gettempdir(),
|
||||
'bottle.*.lock'))
|
||||
for lock_file in lock_files:
|
||||
os.chown(lock_file, user, group)
|
||||
|
||||
drop_privileges_permanently(user, group, ())
|
||||
except Exception:
|
||||
print "Failed to drop privileges. Running with current user."
|
||||
|
||||
|
||||
def dmerge(*args):
|
||||
"""
|
||||
Return new directionay being the sum of all merged dictionaries passed
|
||||
as arguments
|
||||
"""
|
||||
|
||||
dictionary = {}
|
||||
|
||||
for arg in args:
|
||||
dictionary.update(arg)
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
|
||||
|
||||
class SettingsContainer(object):
|
||||
"""
|
||||
Singleton containing the settings for the whole app
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
|
||||
if not cls._instance:
|
||||
cls._instance = super(SettingsContainer, cls).__new__(cls, *args,
|
||||
**kwargs)
|
||||
cls._instance.update_with_module(default_settings)
|
||||
return cls._instance
|
||||
|
||||
|
||||
def update_with_module(self, module):
|
||||
"""
|
||||
Update settings with values from the given module.
|
||||
(Taking only variable with uppercased name)
|
||||
"""
|
||||
for name, value in module.__dict__.iteritems():
|
||||
if name.isupper():
|
||||
setattr(self, name, value)
|
||||
return self
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_module(cls, module):
|
||||
"""
|
||||
Create an instance of SettingsContainer with values based
|
||||
on the one in the passed module.
|
||||
"""
|
||||
settings = cls()
|
||||
settings.update_with_module(module)
|
||||
return settings
|
||||
|
||||
|
||||
def update_with_file(self, filepath):
|
||||
"""
|
||||
Update settings with values from the given module file.
|
||||
User update_with_module() behind the scene
|
||||
"""
|
||||
sys.path.insert(0, os.path.dirname(filepath))
|
||||
module_name = os.path.splitext(os.path.basename(filepath))[0]
|
||||
return self.update_with_module(__import__(module_name))
|
||||
|
||||
|
||||
settings = SettingsContainer()
|
@ -14,11 +14,24 @@
|
||||
<link href="/static/css/bootstrap.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!-- Le HTML5 shim, for IE7-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<!-- Prompt IE 6 users to tell them to install chrome frame -->
|
||||
<!--[if gte IE 0 ]>
|
||||
<script defer src="//ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js"></script>
|
||||
<script defer>window.attachEvent('onload',
|
||||
function() {
|
||||
CFInstall.check({
|
||||
mode: 'overlay',
|
||||
cssText: 'margin-top: 400px;'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="/static/js/jquery-1.7.2.min.js"></script>
|
||||
<script src="/static/js/sjcl.js"></script>
|
||||
<script src="/static/js/behavior.js"></script>
|