1
0
mirror of https://github.com/Tygs/0bin.git synced 2023-08-10 21:13:00 +03:00

Add delete paste

This commit is contained in:
ksamuel 2020-08-11 16:37:03 +02:00
parent e13cead89b
commit 6843a19fa8
12 changed files with 202 additions and 169 deletions

1
.gitignore vendored
View File

@ -37,3 +37,4 @@ content
settings_local.py
build
.vscode
var

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,7 +122,7 @@ 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:
@ -139,7 +137,6 @@ class Paste(object):
with open(counter_file, "w") as fcounter:
fcounter.write(str(counter_value))
def save(self):
"""
Save the content of this paste to a file.
@ -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):
"""

View File

@ -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):

View File

@ -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);

View File

@ -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

View File

@ -16,8 +16,8 @@
<input type="file" class="hide-upload" id="file-upload" @change="handleUpload($event.target.files)">
</p>
<form class="well" method="post" action="/paste/create">
<p class="paste-option">
<form class="well" method="post" action="/paste/create" @submit.prevent="encryptAndSendPaste()">
<p class=" paste-option">
<label for="expiration">Expiration:</label>
<select id="expiration" name="expiration" v-model="newPaste.expiration">
<option value="burn_after_reading">Burn after reading</option>
@ -25,14 +25,14 @@
<option value="1_month">1 month</option>
<option value="never">Never</option>
</select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button>
<button type="submit" class="btn btn-primary" ">Submit</button>
</p>
<p>
<div class="progress progress-striped active" v-show="isLoading">
<div class=" progress progress-striped active" v-show="isLoading">
<div class="bar"></div>
</div>
<textarea rows="10" style="width:100%;" class="input-xlarge" id="content" name="content" autofocus
v-on:keydown.ctrl.enter="encryptAndSendPaste($event)"></textarea>
v-on:keydown.prevent.ctrl.enter="encryptAndSendPaste()"></textarea>
</p>
<p class="paste-option down" v-if="displayBottomToolBar">
<label for="expiration">Expiration:</label>
@ -42,7 +42,7 @@
<option value="1_month">1 month</option>
<option value="never">Never</option>
</select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button>
<button type="submit" class="btn btn-primary">Submit</button>
</p>
</form>

View File

@ -12,11 +12,11 @@
<link rel="shortcut icon" href="/favicon.ico">
%if settings.COMPRESSED_STATIC_FILES:
<link href="/static/css/style.min.css?{{ settings.VERSION }}" rel="stylesheet" />
<link href="/static/css/style.min.css?{{ VERSION }}" rel="stylesheet" />
%else:
<link href="/static/css/prettify.css" rel="stylesheet" />
<link href="/static/css/bootstrap.css" rel="stylesheet">
<link href="/static/css/style.css?{{ settings.VERSION }}" rel="stylesheet">
<link href="/static/css/style.css?{{ VERSION }}" rel="stylesheet">
%end
<!-- Le HTML5 shim, for IE7-8 support of HTML5 elements -->
@ -109,7 +109,7 @@
<p :class="'alert alert-' + msg.type" v-for="msg in messages">
<a class="close" data-dismiss="alert" href="#" @click="$event.target.parentNode.remove()">×</a>
<a class="close" data-dismiss="alert" href="#" @click.prevent="$event.target.parentNode.remove()">×</a>
<strong class="title" v-if="msg.title" v-html="msg.title"></strong>
<span class="message" v-html="msg.content"></span>
<a v-if="msg.action.message" href="#"
@ -156,10 +156,10 @@
<script src="/static/js/vue.js"></script>
%if settings.COMPRESSED_STATIC_FILES:
<script src="/static/js/main.min.js?{{ settings.VERSION }}"></script>
<script src="/static/js/main.min.js?{{ VERSION }}"></script>
%else:
<script src="/static/js/sjcl.js"></script>
<script src="/static/js/behavior.js?{{ settings.VERSION }}"></script>
<script src="/static/js/behavior.js?{{ VERSION }}"></script>
%end
<script type="text/javascript">
@ -168,7 +168,7 @@
</script>
%if settings.COMPRESSED_STATIC_FILES:
<script src="/static/js/additional.min.js?{{ settings.VERSION }}"></script>
<script src="/static/js/additional.min.js?{{ VERSION }}"></script>
%else:
<script src="/static/js/lzw.js"></script>

View File

@ -4,7 +4,7 @@
<input type="file" class="hide-upload" id="file-upload" @change="handleUpload($event.target.files)">
</p>
<form class="well" method="post" action="/paste/create">
<form class="well" method="post" action="/paste/create" @submit.prevent="encryptAndSendPaste()">
<p class="paste-option">
<label for="expiration">Expiration:</label>
<select id="expiration" name="expiration" v-model="newPaste.expiration">
@ -13,14 +13,14 @@
<option value="1_month">1 month</option>
<option value="never">Never</option>
</select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button>
<button type="submit" class="btn btn-primary">Submit</button>
</p>
<p>
<div class="progress progress-striped active" v-show="isLoading">
<div class="bar"></div>
</div>
<textarea rows="10" style="width:100%" class="input-xlarge" id="content" name="content" autofocus
v-on:keydown.ctrl.enter="encryptAndSendPaste($event)"></textarea>
v-on:keydown.prevent.ctrl.enter="encryptAndSendPaste()"></textarea>
</p>
<p class="paste-option down" v-if="displayBottomToolBar">
@ -31,7 +31,7 @@
<option value="1_month">1 month</option>
<option value="never">Never</option>
</select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button>
<button type="submit" class="btn btn-primary">Submit</button>
</p>
</form>

View File

@ -1,7 +1,7 @@
%if "burn_after_reading" in str(paste.expiration):
%if keep_alive:
<p class="alert alert-info">
<a class="close" data-dismiss="alert" href="#" @click="$event.target.parentNode.remove()">×</a>
<a class="close" data-dismiss="alert" href="#" @click.prevent="$event.target.parentNode.remove()">×</a>
<strong class="title">Ok!</strong>
<span class="message">
This paste will be deleted the next time it is read.
@ -9,7 +9,7 @@
</p>
%else:
<p class="alert">
<a class="close" data-dismiss="alert" href="#" @click="$event.target.parentNode.remove()">×</a>
<a class="close" data-dismiss="alert" href="#" @click.prevent="$event.target.parentNode.remove()">×</a>
<strong class="title">Warning!</strong>
<span class="message">
This paste has self-destructed. If you close this window,
@ -54,6 +54,10 @@
</pre>
</p>
<p v-if="currentPaste.ownerKey">
<button type="button" class="btn btn-danger" @click="handleDeletePaste()">Delete this paste</button>
</p>
<p class="paste-option btn-group bottom">
<button class="btn btn-clone" @click.prevent="handleClone()"><i class="icon-camera"></i>&nbsp;Clone</button>
@ -69,7 +73,7 @@
<!-- For cloning -->
<div class="submit-form clone">
<form class="well" method="post" action="/paste/create">
<form class="well" method="post" action="/paste/create" @submit.prevent="encryptAndSendPaste()">
<p class="paste-option">
<label for="expiration">Expiration:</label>
<select id="expiration" name="expiration" v-model="newPaste.expiration">
@ -78,7 +82,7 @@
<option value="1_month">1 month</option>
<option value="never">Never</option>
</select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button>
<button type="submit" class="btn btn-primary">Submit</button>
<button class="btn btn-danger" @click.prevent="handleCancelClone()">Cancel clone</button>
</p>
@ -87,7 +91,7 @@
<div class="bar"></div>
</div>
<textarea rows="10" style="width:100%;" class="input-xlarge" id="content" name="content" autofocus
v-on:keydown.ctrl.enter="encryptAndSendPaste($event)"></textarea>
v-on:keydown.prevent.ctrl.enter="encryptAndSendPaste()"></textarea>
</div>
<p class="paste-option down" v-if="displayBottomToolBar">
@ -98,7 +102,7 @@
<option value="1_month">1 month</option>
<option value="never">Never</option>
</select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button>
<button type="submit" class="btn btn-primary">Submit</button>
</p>
</form>
</div>