diff --git a/.gitignore b/.gitignore index 85b18f7..a901e11 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ content settings_local.py build .vscode +var diff --git a/zerobin/__init__.py b/zerobin/__init__.py index 90eaaba..5c09490 100644 --- a/zerobin/__init__.py +++ b/zerobin/__init__.py @@ -1,8 +1,5 @@ -#!/usr/bin/env python -# coding: utf-8 +from pathlib import Path -from __future__ import absolute_import +__version__ = "1.0.0" -from zerobin.default_settings import VERSION - -__version__ = VERSION +ROOT_DIR = Path(__file__).absolute().parent diff --git a/zerobin/cmd.py b/zerobin/cmd.py index d7d40b1..ffaa78f 100644 --- a/zerobin/cmd.py +++ b/zerobin/cmd.py @@ -7,6 +7,7 @@ import sys import re +import secrets import _thread as thread from zerobin.utils import settings, SettingsValidationError, drop_privileges @@ -19,6 +20,7 @@ import clize def runserver( + *, host="", port="", debug=None, @@ -30,8 +32,6 @@ def runserver( paste_id_length=None, server="cherrypy", ): - - debug = True if version: print("0bin V%s" % settings.VERSION) sys.exit(0) @@ -41,6 +41,15 @@ def runserver( settings.USER = user or settings.USER settings.GROUP = group or settings.GROUP settings.PASTE_ID_LENGTH = paste_id_length or settings.PASTE_ID_LENGTH + settings.DEBUG = bool(debug) if debug is not None else settings.DEBUG + + settings.VAR_DIR.mkdir(exist_ok=True, parents=True) + settings.PASTE_FILES_ROOT.mkdir(exist_ok=True, parents=True) + + secret_key_file = settings.VAR_DIR / "secret_key" + if not secret_key_file.is_file(): + secret_key_file.write_text(secrets.token_urlsafe(64)) + settings.SECRET_KEY = secret_key_file.read_text() try: _, app = get_app(debug, settings_file, compressed_static, settings=settings) @@ -52,14 +61,10 @@ def runserver( if settings.DEBUG: run( - app, - host=settings.HOST, - port=settings.PORT, - reloader=True, - server="cherrypy", + app, host=settings.HOST, port=settings.PORT, reloader=True, server=server, ) else: - run(app, host=settings.HOST, port=settings.PORT, server="cherrypy") + run(app, host=settings.HOST, port=settings.PORT, server=server) # The regex parse the url and separate the paste's id from the decription key @@ -78,7 +83,7 @@ def unpack_paste(paste): return paste -def delete_paste(quiet=False, *pastes): +def delete_paste(*pastes, quiet=False): """ Remove pastes, given its ID or its URL diff --git a/zerobin/default_settings.py b/zerobin/default_settings.py index 8704632..3b4e55b 100644 --- a/zerobin/default_settings.py +++ b/zerobin/default_settings.py @@ -1,28 +1,19 @@ -#!/usr/bin/env python -# coding: utf-8 +from zerobin import ROOT_DIR -from __future__ import unicode_literals, absolute_import +# Path to the directory that will contains all variable content, such +# as pastes, the secret key, etc +VAR_DIR = ROOT_DIR.parent / "var" - -######## NOT SETTINGS, JUST BOILER PLATE ############## -import os - -VERSION = '0.5' -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 +# debug will get you error messages 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 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') +STATIC_FILES_ROOT = ROOT_DIR / "static" # If True, will link the compressed verion of the js and css files, # otherwise, will use the ordinary files @@ -31,15 +22,15 @@ COMPRESSED_STATIC_FILES = False # 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 = VAR_DIR / "content" -# a tuple of absolute paths of directory where to look the template for +# 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 +# if you want to override, it needs to be it a directory at the begining of +# this tuple. By default, custom_views is meant for that purpose. TEMPLATE_DIRS = ( - os.path.join(ROOT_DIR, 'views'), + VAR_DIR / "custom_views", + ROOT_DIR / "views", ) # Port and host the embeded python server should be using @@ -62,10 +53,10 @@ REFRESH_COUNTER = 60 * 1 # Names/links to insert in the menu bar. # Any link with "mailto:" will be escaped to prevent spam MENU = ( - ('Home', '/'), # internal link. First link will be highlited - ('Download 0bin', 'https://github.com/sametmax/0bin'), # external link - ('Faq', '/faq/'), # faq - ('Contact', 'mailto:your@email.com') # email + ("Home", "/"), # internal link. First link will be highlited + ("Download 0bin", "https://github.com/sametmax/0bin"), # external link + ("Faq", "/faq/"), # faq + ("Contact", "mailto:your@email.com"), # email ) # limit size of pasted text in bytes. Be careful allowing too much size can diff --git a/zerobin/paste.py b/zerobin/paste.py index b5316f5..de9b640 100644 --- a/zerobin/paste.py +++ b/zerobin/paste.py @@ -12,7 +12,6 @@ from datetime import datetime, timedelta from zerobin.utils import settings, to_ascii, as_unicode, safe_open as open - class Paste(object): """ A paste object to deal with the file opening/parsing/saving and the @@ -22,14 +21,12 @@ class Paste(object): DIR_CACHE = set() DURATIONS = { - '1_day': 24 * 3600, - '1_month': 30 * 24 * 3600, - 'never': 365 * 24 * 3600 * 100, + "1_day": 24 * 3600, + "1_month": 30 * 24 * 3600, + "never": 365 * 24 * 3600 * 100, } - - def __init__(self, uuid=None, uuid_length=None, - content=None, expiration=None): + def __init__(self, uuid=None, uuid_length=None, content=None, expiration=None): self.content = content self.expiration = self.get_expiration(expiration) @@ -37,15 +34,14 @@ class Paste(object): if not uuid: # generate the uuid from the decoded content by hashing it # and turning it into base64, with some characters strippped - uuid = hashlib.sha1(self.content.encode('utf8')) + uuid = hashlib.sha1(self.content.encode("utf8")) uuid = base64.b64encode(uuid.digest()).decode() - uuid = uuid.rstrip('=\n').replace('/', '-') + uuid = uuid.rstrip("=\n").replace("/", "-") if uuid_length: uuid = uuid[:uuid_length] self.uuid = uuid - def get_expiration(self, expiration): """ Return a date at which the Paste will expire @@ -60,7 +56,6 @@ class Paste(object): except KeyError: return expiration - @classmethod def build_path(cls, *dirs): """ @@ -69,7 +64,6 @@ class Paste(object): """ return os.path.join(settings.PASTE_FILES_ROOT, *dirs) - @classmethod def get_path(cls, uuid): """ @@ -77,7 +71,6 @@ class Paste(object): """ return cls.build_path(uuid[:2], uuid[2:4], uuid) - @property def path(self): """ @@ -85,6 +78,13 @@ class Paste(object): """ return self.get_path(self.uuid) + @property + def owner_key(self): + """ + Return a key that gives you admin rights on this paste + """ + payload = (settings.SECRET_KEY + self.uuid).encode("ascii") + return hashlib.sha256(payload).hexdigest() @classmethod def load_from_file(cls, path): @@ -98,16 +98,15 @@ class Paste(object): expiration = next(paste).strip() content = next(paste).strip() if "burn_after_reading" not in expiration: - expiration = datetime.strptime(expiration, '%Y-%m-%d %H:%M:%S.%f') + expiration = datetime.strptime(expiration, "%Y-%m-%d %H:%M:%S.%f") except StopIteration: - raise TypeError(to_ascii('File %s is malformed' % path)) + raise TypeError(to_ascii("File %s is malformed" % path)) except (IOError, OSError): - raise ValueError(to_ascii('Can not open paste from file %s' % path)) + raise ValueError(to_ascii("Can not open paste from file %s" % path)) return Paste(uuid=uuid, expiration=expiration, content=content) - @classmethod def load(cls, uuid): """ @@ -116,7 +115,6 @@ class Paste(object): """ return cls.load_from_file(cls.get_path(uuid)) - def increment_counter(self): """ Increment pastes counter. @@ -124,21 +122,20 @@ class Paste(object): It uses a lock file to prevent multi access to the file. """ path = settings.PASTE_FILES_ROOT - counter_file = os.path.join(path, 'counter') + counter_file = os.path.join(path, "counter") lock = lockfile.LockFile(counter_file) with lock: - # Read the value from the counter - try: - with open(counter_file, "r") as fcounter: - counter_value = int(fcounter.read(50)) + 1 - except (ValueError, IOError, OSError): - counter_value = 1 - - # write new value to counter - with open(counter_file, "w") as fcounter: - fcounter.write(str(counter_value)) + # Read the value from the counter + try: + with open(counter_file, "r") as fcounter: + counter_value = int(fcounter.read(50)) + 1 + except (ValueError, IOError, OSError): + counter_value = 1 + # write new value to counter + with open(counter_file, "w") as fcounter: + fcounter.write(str(counter_value)) def save(self): """ @@ -171,18 +168,17 @@ class Paste(object): # a quick period of time where you can redirect to the page without # deleting the paste if "burn_after_reading" == self.expiration: - expiration = self.expiration + '#%s' % datetime.now() # TODO: use UTC dates + expiration = self.expiration + "#%s" % datetime.now() # TODO: use UTC dates else: expiration = as_unicode(self.expiration) # write the paste - with open(self.path, 'w') as f: - f.write(expiration + '\n') - f.write(self.content + '\n') + with open(self.path, "w") as f: + f.write(expiration + "\n") + f.write(self.content + "\n") return self - @classmethod def get_pastes_count(cls): """ @@ -190,14 +186,13 @@ class Paste(object): (must have option DISPLAY_COUNTER enabled for the pastes to be be counted) """ - counter_file = os.path.join(settings.PASTE_FILES_ROOT, 'counter') + counter_file = os.path.join(settings.PASTE_FILES_ROOT, "counter") try: count = int(open(counter_file).read(50)) except (IOError, OSError): count = 0 - return '{0:,}'.format(count) - + return "{0:,}".format(count) @property def humanized_expiration(self): @@ -215,19 +210,18 @@ class Paste(object): return None if expiration < 60: - return 'in %s s' % expiration + return "in %s s" % expiration if expiration < 60 * 60: - return 'in %s m' % int(expiration / 60) + return "in %s m" % int(expiration / 60) if expiration < 60 * 60 * 24: - return 'in %s h' % int(expiration / (60 * 60)) + return "in %s h" % int(expiration / (60 * 60)) if expiration < 60 * 60 * 24 * 10: - return 'in %s days(s)' % int(expiration / (60 * 60 * 24)) - - return 'the %s' % self.expiration.strftime('%m/%d/%Y') + return "in %s days(s)" % int(expiration / (60 * 60 * 24)) + return "the %s" % self.expiration.strftime("%m/%d/%Y") def delete(self): """ diff --git a/zerobin/routes.py b/zerobin/routes.py index 6fd5321..4bc3101 100644 --- a/zerobin/routes.py +++ b/zerobin/routes.py @@ -1,9 +1,3 @@ -#!/usr/bin/env python -# coding: utf-8 - -from __future__ import unicode_literals, absolute_import, print_function -import pdb - """ Script including controller, rooting, and dependency management. """ @@ -11,24 +5,17 @@ import pdb import os import sys -try: - import thread -except ImportError: - import _thread as thread +import _thread as thread -try: - import urlparse -except ImportError: - import urllib.parse as urlparse +import urllib.parse as urlparse from datetime import datetime, timedelta -# add project dir and libs dir to the PYTHON PATH to ensure they are -# importable -from zerobin.utils import settings, SettingsValidationError, drop_privileges, dmerge +from zerobin import __version__ +from zerobin.utils import settings, SettingsValidationError, dmerge import bottle -from bottle import Bottle, run, static_file, view, request +from bottle import Bottle, static_file, view, request, HTTPResponse from zerobin.paste import Paste @@ -36,6 +23,7 @@ from zerobin.paste import Paste app = Bottle() GLOBAL_CONTEXT = { "settings": settings, + "VERSION": __version__, "pastes_count": Paste.get_pastes_count(), "refresh_counter": datetime.now(), } @@ -99,7 +87,7 @@ def create_paste(): GLOBAL_CONTEXT["pastes_count"] = Paste.get_pastes_count() GLOBAL_CONTEXT["refresh_counter"] = now - return {"status": "ok", "paste": paste.uuid} + return {"status": "ok", "paste": paste.uuid, "owner_key": paste.owner_key} return { "status": "error", @@ -107,7 +95,7 @@ def create_paste(): } -@app.route("/paste/:paste_id") +@app.route("/paste/:paste_id", method="GET") @view("paste") def display_paste(paste_id): @@ -141,6 +129,25 @@ def display_paste(paste_id): return dmerge(context, GLOBAL_CONTEXT) +@app.route("/paste/:paste_id", method="DELETE") +def delete_paste(paste_id): + + try: + paste = Paste.load(paste_id) + except (TypeError, ValueError): + return error404(ValueError) + + if paste.owner_key != request.forms.get("owner_key", None): + return HTTPResponse(status=403, body="Wrong owner key") + + paste.delete() + + return { + "status": "ok", + "message": "Paste deleted", + } + + @app.error(404) @view("404") def error404(code): diff --git a/zerobin/static/js/behavior.js b/zerobin/static/js/behavior.js index a439ee2..f5b615c 100644 --- a/zerobin/static/js/behavior.js +++ b/zerobin/static/js/behavior.js @@ -1,12 +1,16 @@ -/*global sjcl:true, jQuery:true, lzw:true, zerobin:true, prettyPrint:true, confirm:true */ +/*global sjcl:true, jQuery:true, lzw:true, zerobin:true, prettyPrint:true */ /* - This file has been migrated away from jQuery, to Vue. Because of the way the code base used to be, a lot of the operation are still using imperative DOM manipulation - instead of the Vue declarative style. We haven't had the time to rewrite it completly and it's a bit of a mixed bag at the moment + This file has been migrated away from jQuery, to Vue. Because of the way + the code base used to be, a lot of the operations are still using imperative + DOM manipulation instead of the Vue declarative style. We haven't had the + time to rewrite it completly and it's a bit of a mixed bag at the moment. */ /* Start random number generator seeding ASAP */ sjcl.random.startCollectors(); + +// Vue template syntax conflicts with bottle template syntax Vue.options.delimiters = ['{%', '%}']; // Force focus for textarea (firefox hack) @@ -14,11 +18,11 @@ setTimeout(function () { document.querySelector('textarea').focus() }, 100) +// Parse obfuscaded emails and make them usable const menu = new Vue({ el: "#menu-top", methods: { formatEmail: (email) => { - /* Parse obfuscaded emails and make them usable */ return "mailto:" + email.replace('__AT__', '@'); }, } @@ -32,6 +36,10 @@ const app = new Vue({ downloadLink: {}, displayBottomToolBar: false, isUploading: false, + currentPaste: { + ownerKey: '', + id: '' + }, newPaste: { expiration: '1_day', content: '', @@ -89,32 +97,60 @@ const app = new Vue({ }, handleUpload: (files) => { - try { app.isUploading = true; zerobin.upload(files); } catch (e) { zerobin.message('error', 'Could no upload the file', 'Error'); } - app.isUploading = false; }, handleForceColoration: (e) => { - /* Force text coloration when clickin on link */ - let content = document.getElementById('paste-content'); content.classList.add('linenums'); e.target.innerHTML = 'Applying coloration'; prettyPrint(); - e.target.remove() - + e.target.parentNode.remove() }, handleSendByEmail: (e) => { e.target.href = 'mailto:friend@example.com?body=' + window.location.toString(); }, + handleDeletePaste: () => { + if (window.confirm("Delete this paste?")) { + app.isLoading = true; + bar.set('Deleting paste...', '50%'); + + fetch('/paste/' + app.currentPaste.id, { + method: "DELETE", + body: new URLSearchParams({ + "owner_key": app.currentPaste.ownerKey + }) + }).then(function (response) { + if (response.ok) { + window.location = "/"; + window.reload() + } else { + form.forEach((node) => node.disabled = false); + app.isLoading = false + zerobin.message( + 'error', + 'Paste could not be deleted. Please try again later.', + 'Error'); + } + app.isLoading = false; + }).catch(function (error) { + zerobin.message( + 'error', + 'Paste could not be delete. Please try again later.', + 'Error'); + app.isLoading = false; + }); + } + }, + copyToClipboard: () => { var pasteContent = zerobin.getPasteContent(); @@ -153,7 +189,6 @@ const app = new Vue({ encryptAndSendPaste: (e) => { - e.preventDefault(); var paste = document.querySelector('textarea').value; if (paste.trim()) { @@ -216,11 +251,10 @@ const app = new Vue({ form.forEach((node) => node.disabled = false); app.isLoading = false } else { - var paste_url = '/paste/' + data.paste + '#' + key; if (app.support.localStorage) { - zerobin.storePaste(paste_url); + zerobin.storePaste('/paste/' + data.paste + "?owner_key=" + data.owner_key + '#' + key); } - window.location = (paste_url); + window.location = ('/paste/' + data.paste + '#' + key); } }) @@ -253,9 +287,9 @@ const app = new Vue({ } }) -/*************************** - **** 0bin utilities *** - ***************************/ +/**************************** + **** 0bin utilities **** + ****************************/ window.zerobin = { /** Base64 + compress + encrypt, with callbacks before each operation, @@ -416,11 +450,11 @@ window.zerobin = { /** Return a reverse sorted list of all the keys in local storage that are prefixed with with the passed version (default being this lib version) */ - getLocalStorageKeys: function () { - var version = 'zerobinV0.1'; + getLocalStorageURLKeys: function () { + var version = 'zerobinV' + zerobin.version; var keys = []; for (var key in localStorage) { - if (key.indexOf(version) !== -1) { + if (key.indexOf(version) !== -1 && key.indexOf("owner_key") === -1) { keys.push(key); } } @@ -436,13 +470,14 @@ window.zerobin = { date = date || new Date(); date = (date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + zerobin.getFormatedTime(date)); - var keys = zerobin.getLocalStorageKeys(); + var keys = zerobin.getLocalStorageURLKeys(); if (localStorage.length > 19) { void localStorage.removeItem(keys[19]); } localStorage.setItem('zerobinV' + zerobin.version + "#" + date, url); + localStorage.setItem('zerobinV' + zerobin.version + "#" + zerobin.getPasteId(url) + "#owner_key", zerobin.getPasteOwnerKey(url)); }, /** Return a list of the previous paste url with the creation date @@ -450,7 +485,7 @@ window.zerobin = { else it should be "the mm-dd-yyy" */ getPreviousPastes: function () { - var keys = zerobin.getLocalStorageKeys(), + var keys = zerobin.getLocalStorageURLKeys(), today = zerobin.getFormatedDate(); return keys.map(function (key, i) { @@ -463,11 +498,14 @@ window.zerobin = { prefix = 'at '; } let link = localStorage.getItem(key); + return { displayDate: displayDate, prefix: prefix, - link: link, - isCurrent: link.replace(/#[^#]+/, '') === window.location.pathname + // The owner key is stored in the URL, but we don't want the user + // to see it + link: link.replace(/\?[^#]+#/, '#'), + isCurrent: link.replace(/\?[^?]+/, '') === window.location.pathname }; }); @@ -497,6 +535,11 @@ window.zerobin = { return loc.pathname.replace(/\/|paste/g, ''); }, + getPasteOwnerKey: function (url) { + var loc = url ? zerobin.parseUrl(url) : window.location; + return (new URLSearchParams(loc.search)).get("owner_key"); + }, + getPasteKey: function (url) { var loc = url ? zerobin.parseUrl(url) : window.location; return loc.hash.replace('#', '').replace(/(\?|&).*$/, ''); @@ -541,7 +584,7 @@ window.zerobin = { title: title, content: message, type: type, - action: action, + action: action || {}, }); callback && callback() }, @@ -681,6 +724,7 @@ let content = ''; if (pasteContent) { content = pasteContent.textContent.trim(); + app.currentPaste.id = zerobin.getPasteId(window.location); } var key = zerobin.getPasteKey(); @@ -788,6 +832,7 @@ window.onload = function () { /* Display previous pastes */ if (app.support.localStorage) { app.previousPastes = zerobin.getPreviousPastes(); + app.currentPaste.ownerKey = localStorage.getItem('zerobinV' + zerobin.version + "#" + zerobin.getPasteId(window.location) + "#owner_key"); } /* Upload file using HTML5 File API */ @@ -804,7 +849,7 @@ if (app.support.fileUpload) { /* Remove expired pasted from history */ if (app.support.history && zerobin.paste_not_found) { var paste_id = zerobin.getPasteId(); - var keys = zerobin.getLocalStorageKeys(); + var keys = zerobin.getLocalStorageURLKeys(); keys.forEach((key, i) => { if (localStorage[key].indexOf(paste_id) !== -1) { localStorage.removeItem(key); diff --git a/zerobin/utils.py b/zerobin/utils.py index 0af6d17..ac92e4d 100644 --- a/zerobin/utils.py +++ b/zerobin/utils.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import print_function, unicode_literals, absolute_import - import time import os import glob @@ -12,7 +8,6 @@ import unicodedata from functools import partial from zerobin import default_settings -sys.path.append(default_settings.LIBS_DIR) try: from zerobin.privilege import drop_privileges_permanently, coerce_user, coerce_group @@ -24,7 +19,7 @@ try: except ImportError: # python-2.6 or earlier - use simplier less-optimized execfile() def run_path(file_path): - mod_globals = {'__file__': file_path} + mod_globals = {"__file__": file_path} execfile(file_path, mod_globals) return mod_globals @@ -43,8 +38,7 @@ def drop_privileges(user=None, group=None, wait=5): user = coerce_user(user) group = coerce_group(group) - lock_files = glob.glob(os.path.join(tempfile.gettempdir(), - 'bottle.*.lock')) + lock_files = glob.glob(os.path.join(tempfile.gettempdir(), "bottle.*.lock")) for lock_file in lock_files: os.chown(lock_file, user, group) @@ -78,12 +72,10 @@ class SettingsContainer(object): def __new__(cls, *args, **kwargs): if not cls._instance: - cls._instance = super(SettingsContainer, cls).__new__(cls, *args, - **kwargs) + cls._instance = super(SettingsContainer, cls).__new__(cls, *args, **kwargs) cls._instance.update_with_module(default_settings) return cls._instance - def update_with_dict(self, dict): """ Update settings with values from the given mapping object. @@ -94,7 +86,6 @@ class SettingsContainer(object): setattr(self, name, value) return self - def update_with_module(self, module): """ Update settings with values from the given module. @@ -102,7 +93,6 @@ class SettingsContainer(object): """ return self.update_with_dict(module.__dict__) - @classmethod def from_module(cls, module): """ @@ -113,7 +103,6 @@ class SettingsContainer(object): settings.update_with_module(module) return settings - def update_with_file(self, filepath): """ Update settings with values from the given module file. @@ -132,7 +121,7 @@ def to_ascii(utext): Try to replace non ASCII char by similar ASCII char. If it can't, replace it with "?". """ - return unicodedata.normalize('NFKD', utext).encode('ascii', "replace") + return unicodedata.normalize("NFKD", utext).encode("ascii", "replace") # Make sure to always specify encoding when using open in Python 2 or 3 diff --git a/zerobin/views/404.tpl b/zerobin/views/404.tpl index 6b6c5d3..5ed0796 100644 --- a/zerobin/views/404.tpl +++ b/zerobin/views/404.tpl @@ -16,8 +16,8 @@

-
-

+ +

- +

-

+
-
- +
+

@@ -42,7 +42,7 @@ - +

diff --git a/zerobin/views/base.tpl b/zerobin/views/base.tpl index 0e0e559..c7d3fea 100644 --- a/zerobin/views/base.tpl +++ b/zerobin/views/base.tpl @@ -12,11 +12,11 @@ %if settings.COMPRESSED_STATIC_FILES: - + %else: - + %end @@ -109,7 +109,7 @@

- × + × %if settings.COMPRESSED_STATIC_FILES: - + %else: - + %end %if settings.COMPRESSED_STATIC_FILES: - + %else: diff --git a/zerobin/views/home.tpl b/zerobin/views/home.tpl index cf0e80a..991bb40 100644 --- a/zerobin/views/home.tpl +++ b/zerobin/views/home.tpl @@ -4,7 +4,7 @@

-
+

- +

+ v-on:keydown.prevent.ctrl.enter="encryptAndSendPaste()">

@@ -31,7 +31,7 @@ - +

diff --git a/zerobin/views/paste.tpl b/zerobin/views/paste.tpl index b26df49..9ab808e 100644 --- a/zerobin/views/paste.tpl +++ b/zerobin/views/paste.tpl @@ -1,7 +1,7 @@ %if "burn_after_reading" in str(paste.expiration): %if keep_alive:

- × + × Ok! This paste will be deleted the next time it is read. @@ -9,7 +9,7 @@

%else:

- × + × Warning! This paste has self-destructed. If you close this window, @@ -54,6 +54,10 @@

+

+ +

+

@@ -69,7 +73,7 @@

-
+

- +

@@ -87,7 +91,7 @@
+ v-on:keydown.prevent.ctrl.enter="encryptAndSendPaste()">

@@ -98,7 +102,7 @@ - +