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 settings_local.py
build build
.vscode .vscode
var

View File

@ -1,8 +1,5 @@
#!/usr/bin/env python from pathlib import Path
# coding: utf-8
from __future__ import absolute_import __version__ = "1.0.0"
from zerobin.default_settings import VERSION ROOT_DIR = Path(__file__).absolute().parent
__version__ = VERSION

View File

@ -7,6 +7,7 @@
import sys import sys
import re import re
import secrets
import _thread as thread import _thread as thread
from zerobin.utils import settings, SettingsValidationError, drop_privileges from zerobin.utils import settings, SettingsValidationError, drop_privileges
@ -19,6 +20,7 @@ import clize
def runserver( def runserver(
*,
host="", host="",
port="", port="",
debug=None, debug=None,
@ -30,8 +32,6 @@ def runserver(
paste_id_length=None, paste_id_length=None,
server="cherrypy", server="cherrypy",
): ):
debug = True
if version: if version:
print("0bin V%s" % settings.VERSION) print("0bin V%s" % settings.VERSION)
sys.exit(0) sys.exit(0)
@ -41,6 +41,15 @@ def runserver(
settings.USER = user or settings.USER settings.USER = user or settings.USER
settings.GROUP = group or settings.GROUP settings.GROUP = group or settings.GROUP
settings.PASTE_ID_LENGTH = paste_id_length or settings.PASTE_ID_LENGTH 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: try:
_, app = get_app(debug, settings_file, compressed_static, settings=settings) _, app = get_app(debug, settings_file, compressed_static, settings=settings)
@ -52,14 +61,10 @@ def runserver(
if settings.DEBUG: if settings.DEBUG:
run( run(
app, app, host=settings.HOST, port=settings.PORT, reloader=True, server=server,
host=settings.HOST,
port=settings.PORT,
reloader=True,
server="cherrypy",
) )
else: 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 # 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 return paste
def delete_paste(quiet=False, *pastes): def delete_paste(*pastes, quiet=False):
""" """
Remove pastes, given its ID or its URL Remove pastes, given its ID or its URL

View File

@ -1,28 +1,19 @@
#!/usr/bin/env python from zerobin import ROOT_DIR
# coding: utf-8
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"
# debug will get you error messages and auto reload
######## 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
# don't set this to True in production # don't set this to True in production
DEBUG = False DEBUG = False
# Should the application serve static files on it's own ? # 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 # If no, set it to None
# In dev this is handy, in prod you probably want the HTTP servers # 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. # 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, # If True, will link the compressed verion of the js and css files,
# otherwise, will use the ordinary files # otherwise, will use the ordinary files
@ -31,15 +22,15 @@ COMPRESSED_STATIC_FILES = False
# absolute path where the paste files should be store # absolute path where the paste files should be store
# default in projectdirectory/static/content/ # default in projectdirectory/static/content/
# use "/" even under Windows # 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 # the first one will be the first to be looked into
# if you want to override a template, create a new dir, write the # if you want to override, it needs to be it a directory at the begining of
# template with the same name as the one you want to override in it # this tuple. By default, custom_views is meant for that purpose.
# then add the dir path at the top of this tuple
TEMPLATE_DIRS = ( 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 # 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. # Names/links to insert in the menu bar.
# Any link with "mailto:" will be escaped to prevent spam # Any link with "mailto:" will be escaped to prevent spam
MENU = ( MENU = (
('Home', '/'), # internal link. First link will be highlited ("Home", "/"), # internal link. First link will be highlited
('Download 0bin', 'https://github.com/sametmax/0bin'), # external link ("Download 0bin", "https://github.com/sametmax/0bin"), # external link
('Faq', '/faq/'), # faq ("Faq", "/faq/"), # faq
('Contact', 'mailto:your@email.com') # email ("Contact", "mailto:your@email.com"), # email
) )
# limit size of pasted text in bytes. Be careful allowing too much size can # 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 from zerobin.utils import settings, to_ascii, as_unicode, safe_open as open
class Paste(object): class Paste(object):
""" """
A paste object to deal with the file opening/parsing/saving and the A paste object to deal with the file opening/parsing/saving and the
@ -22,14 +21,12 @@ class Paste(object):
DIR_CACHE = set() DIR_CACHE = set()
DURATIONS = { DURATIONS = {
'1_day': 24 * 3600, "1_day": 24 * 3600,
'1_month': 30 * 24 * 3600, "1_month": 30 * 24 * 3600,
'never': 365 * 24 * 3600 * 100, "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.content = content
self.expiration = self.get_expiration(expiration) self.expiration = self.get_expiration(expiration)
@ -37,15 +34,14 @@ class Paste(object):
if not uuid: if not uuid:
# generate the uuid from the decoded content by hashing it # generate the uuid from the decoded content by hashing it
# and turning it into base64, with some characters strippped # 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 = base64.b64encode(uuid.digest()).decode()
uuid = uuid.rstrip('=\n').replace('/', '-') uuid = uuid.rstrip("=\n").replace("/", "-")
if uuid_length: if uuid_length:
uuid = uuid[:uuid_length] uuid = uuid[:uuid_length]
self.uuid = uuid self.uuid = uuid
def get_expiration(self, expiration): def get_expiration(self, expiration):
""" """
Return a date at which the Paste will expire Return a date at which the Paste will expire
@ -60,7 +56,6 @@ class Paste(object):
except KeyError: except KeyError:
return expiration return expiration
@classmethod @classmethod
def build_path(cls, *dirs): def build_path(cls, *dirs):
""" """
@ -69,7 +64,6 @@ class Paste(object):
""" """
return os.path.join(settings.PASTE_FILES_ROOT, *dirs) return os.path.join(settings.PASTE_FILES_ROOT, *dirs)
@classmethod @classmethod
def get_path(cls, uuid): def get_path(cls, uuid):
""" """
@ -77,7 +71,6 @@ class Paste(object):
""" """
return cls.build_path(uuid[:2], uuid[2:4], uuid) return cls.build_path(uuid[:2], uuid[2:4], uuid)
@property @property
def path(self): def path(self):
""" """
@ -85,6 +78,13 @@ class Paste(object):
""" """
return self.get_path(self.uuid) 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 @classmethod
def load_from_file(cls, path): def load_from_file(cls, path):
@ -98,16 +98,15 @@ class Paste(object):
expiration = next(paste).strip() expiration = next(paste).strip()
content = next(paste).strip() content = next(paste).strip()
if "burn_after_reading" not in expiration: 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: except StopIteration:
raise TypeError(to_ascii('File %s is malformed' % path)) raise TypeError(to_ascii("File %s is malformed" % path))
except (IOError, OSError): 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) return Paste(uuid=uuid, expiration=expiration, content=content)
@classmethod @classmethod
def load(cls, uuid): def load(cls, uuid):
""" """
@ -116,7 +115,6 @@ class Paste(object):
""" """
return cls.load_from_file(cls.get_path(uuid)) return cls.load_from_file(cls.get_path(uuid))
def increment_counter(self): def increment_counter(self):
""" """
Increment pastes counter. Increment pastes counter.
@ -124,21 +122,20 @@ class Paste(object):
It uses a lock file to prevent multi access to the file. It uses a lock file to prevent multi access to the file.
""" """
path = settings.PASTE_FILES_ROOT path = settings.PASTE_FILES_ROOT
counter_file = os.path.join(path, 'counter') counter_file = os.path.join(path, "counter")
lock = lockfile.LockFile(counter_file) lock = lockfile.LockFile(counter_file)
with lock: with lock:
# Read the value from the counter # Read the value from the counter
try: try:
with open(counter_file, "r") as fcounter: with open(counter_file, "r") as fcounter:
counter_value = int(fcounter.read(50)) + 1 counter_value = int(fcounter.read(50)) + 1
except (ValueError, IOError, OSError): except (ValueError, IOError, OSError):
counter_value = 1 counter_value = 1
# write new value to counter
with open(counter_file, "w") as fcounter:
fcounter.write(str(counter_value))
# write new value to counter
with open(counter_file, "w") as fcounter:
fcounter.write(str(counter_value))
def save(self): def save(self):
""" """
@ -171,18 +168,17 @@ class Paste(object):
# a quick period of time where you can redirect to the page without # a quick period of time where you can redirect to the page without
# deleting the paste # deleting the paste
if "burn_after_reading" == self.expiration: 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: else:
expiration = as_unicode(self.expiration) expiration = as_unicode(self.expiration)
# write the paste # write the paste
with open(self.path, 'w') as f: with open(self.path, "w") as f:
f.write(expiration + '\n') f.write(expiration + "\n")
f.write(self.content + '\n') f.write(self.content + "\n")
return self return self
@classmethod @classmethod
def get_pastes_count(cls): def get_pastes_count(cls):
""" """
@ -190,14 +186,13 @@ class Paste(object):
(must have option DISPLAY_COUNTER enabled for the pastes to be (must have option DISPLAY_COUNTER enabled for the pastes to be
be counted) be counted)
""" """
counter_file = os.path.join(settings.PASTE_FILES_ROOT, 'counter') counter_file = os.path.join(settings.PASTE_FILES_ROOT, "counter")
try: try:
count = int(open(counter_file).read(50)) count = int(open(counter_file).read(50))
except (IOError, OSError): except (IOError, OSError):
count = 0 count = 0
return '{0:,}'.format(count) return "{0:,}".format(count)
@property @property
def humanized_expiration(self): def humanized_expiration(self):
@ -215,19 +210,18 @@ class Paste(object):
return None return None
if expiration < 60: if expiration < 60:
return 'in %s s' % expiration return "in %s s" % expiration
if expiration < 60 * 60: if expiration < 60 * 60:
return 'in %s m' % int(expiration / 60) return "in %s m" % int(expiration / 60)
if expiration < 60 * 60 * 24: 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: if expiration < 60 * 60 * 24 * 10:
return 'in %s days(s)' % int(expiration / (60 * 60 * 24)) return "in %s days(s)" % int(expiration / (60 * 60 * 24))
return 'the %s' % self.expiration.strftime('%m/%d/%Y')
return "the %s" % self.expiration.strftime("%m/%d/%Y")
def delete(self): 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. Script including controller, rooting, and dependency management.
""" """
@ -11,24 +5,17 @@ import pdb
import os import os
import sys import sys
try: import _thread as thread
import thread
except ImportError:
import _thread as thread
try: import urllib.parse as urlparse
import urlparse
except ImportError:
import urllib.parse as urlparse
from datetime import datetime, timedelta from datetime import datetime, timedelta
# add project dir and libs dir to the PYTHON PATH to ensure they are from zerobin import __version__
# importable from zerobin.utils import settings, SettingsValidationError, dmerge
from zerobin.utils import settings, SettingsValidationError, drop_privileges, dmerge
import bottle 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 from zerobin.paste import Paste
@ -36,6 +23,7 @@ from zerobin.paste import Paste
app = Bottle() app = Bottle()
GLOBAL_CONTEXT = { GLOBAL_CONTEXT = {
"settings": settings, "settings": settings,
"VERSION": __version__,
"pastes_count": Paste.get_pastes_count(), "pastes_count": Paste.get_pastes_count(),
"refresh_counter": datetime.now(), "refresh_counter": datetime.now(),
} }
@ -99,7 +87,7 @@ def create_paste():
GLOBAL_CONTEXT["pastes_count"] = Paste.get_pastes_count() GLOBAL_CONTEXT["pastes_count"] = Paste.get_pastes_count()
GLOBAL_CONTEXT["refresh_counter"] = now GLOBAL_CONTEXT["refresh_counter"] = now
return {"status": "ok", "paste": paste.uuid} return {"status": "ok", "paste": paste.uuid, "owner_key": paste.owner_key}
return { return {
"status": "error", "status": "error",
@ -107,7 +95,7 @@ def create_paste():
} }
@app.route("/paste/:paste_id") @app.route("/paste/:paste_id", method="GET")
@view("paste") @view("paste")
def display_paste(paste_id): def display_paste(paste_id):
@ -141,6 +129,25 @@ def display_paste(paste_id):
return dmerge(context, GLOBAL_CONTEXT) 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) @app.error(404)
@view("404") @view("404")
def error404(code): 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 This file has been migrated away from jQuery, to Vue. Because of the way
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 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 */ /* Start random number generator seeding ASAP */
sjcl.random.startCollectors(); sjcl.random.startCollectors();
// Vue template syntax conflicts with bottle template syntax
Vue.options.delimiters = ['{%', '%}']; Vue.options.delimiters = ['{%', '%}'];
// Force focus for textarea (firefox hack) // Force focus for textarea (firefox hack)
@ -14,11 +18,11 @@ setTimeout(function () {
document.querySelector('textarea').focus() document.querySelector('textarea').focus()
}, 100) }, 100)
// Parse obfuscaded emails and make them usable
const menu = new Vue({ const menu = new Vue({
el: "#menu-top", el: "#menu-top",
methods: { methods: {
formatEmail: (email) => { formatEmail: (email) => {
/* Parse obfuscaded emails and make them usable */
return "mailto:" + email.replace('__AT__', '@'); return "mailto:" + email.replace('__AT__', '@');
}, },
} }
@ -32,6 +36,10 @@ const app = new Vue({
downloadLink: {}, downloadLink: {},
displayBottomToolBar: false, displayBottomToolBar: false,
isUploading: false, isUploading: false,
currentPaste: {
ownerKey: '',
id: ''
},
newPaste: { newPaste: {
expiration: '1_day', expiration: '1_day',
content: '', content: '',
@ -89,32 +97,60 @@ const app = new Vue({
}, },
handleUpload: (files) => { handleUpload: (files) => {
try { try {
app.isUploading = true; app.isUploading = true;
zerobin.upload(files); zerobin.upload(files);
} catch (e) { } catch (e) {
zerobin.message('error', 'Could no upload the file', 'Error'); zerobin.message('error', 'Could no upload the file', 'Error');
} }
app.isUploading = false; app.isUploading = false;
}, },
handleForceColoration: (e) => { handleForceColoration: (e) => {
/* Force text coloration when clickin on link */
let content = document.getElementById('paste-content'); let content = document.getElementById('paste-content');
content.classList.add('linenums'); content.classList.add('linenums');
e.target.innerHTML = 'Applying coloration'; e.target.innerHTML = 'Applying coloration';
prettyPrint(); prettyPrint();
e.target.remove() e.target.parentNode.remove()
}, },
handleSendByEmail: (e) => { handleSendByEmail: (e) => {
e.target.href = 'mailto:friend@example.com?body=' + window.location.toString(); 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: () => { copyToClipboard: () => {
var pasteContent = zerobin.getPasteContent(); var pasteContent = zerobin.getPasteContent();
@ -153,7 +189,6 @@ const app = new Vue({
encryptAndSendPaste: (e) => { encryptAndSendPaste: (e) => {
e.preventDefault();
var paste = document.querySelector('textarea').value; var paste = document.querySelector('textarea').value;
if (paste.trim()) { if (paste.trim()) {
@ -216,11 +251,10 @@ const app = new Vue({
form.forEach((node) => node.disabled = false); form.forEach((node) => node.disabled = false);
app.isLoading = false app.isLoading = false
} else { } else {
var paste_url = '/paste/' + data.paste + '#' + key;
if (app.support.localStorage) { 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 = { window.zerobin = {
/** Base64 + compress + encrypt, with callbacks before each operation, /** 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 /** Return a reverse sorted list of all the keys in local storage that
are prefixed with with the passed version (default being this lib are prefixed with with the passed version (default being this lib
version) */ version) */
getLocalStorageKeys: function () { getLocalStorageURLKeys: function () {
var version = 'zerobinV0.1'; var version = 'zerobinV' + zerobin.version;
var keys = []; var keys = [];
for (var key in localStorage) { for (var key in localStorage) {
if (key.indexOf(version) !== -1) { if (key.indexOf(version) !== -1 && key.indexOf("owner_key") === -1) {
keys.push(key); keys.push(key);
} }
} }
@ -436,13 +470,14 @@ window.zerobin = {
date = date || new Date(); date = date || new Date();
date = (date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + zerobin.getFormatedTime(date)); date = (date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + zerobin.getFormatedTime(date));
var keys = zerobin.getLocalStorageKeys(); var keys = zerobin.getLocalStorageURLKeys();
if (localStorage.length > 19) { if (localStorage.length > 19) {
void localStorage.removeItem(keys[19]); void localStorage.removeItem(keys[19]);
} }
localStorage.setItem('zerobinV' + zerobin.version + "#" + date, url); 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 /** 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" else it should be "the mm-dd-yyy"
*/ */
getPreviousPastes: function () { getPreviousPastes: function () {
var keys = zerobin.getLocalStorageKeys(), var keys = zerobin.getLocalStorageURLKeys(),
today = zerobin.getFormatedDate(); today = zerobin.getFormatedDate();
return keys.map(function (key, i) { return keys.map(function (key, i) {
@ -463,11 +498,14 @@ window.zerobin = {
prefix = 'at '; prefix = 'at ';
} }
let link = localStorage.getItem(key); let link = localStorage.getItem(key);
return { return {
displayDate: displayDate, displayDate: displayDate,
prefix: prefix, prefix: prefix,
link: link, // The owner key is stored in the URL, but we don't want the user
isCurrent: link.replace(/#[^#]+/, '') === window.location.pathname // to see it
link: link.replace(/\?[^#]+#/, '#'),
isCurrent: link.replace(/\?[^?]+/, '') === window.location.pathname
}; };
}); });
@ -497,6 +535,11 @@ window.zerobin = {
return loc.pathname.replace(/\/|paste/g, ''); 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) { getPasteKey: function (url) {
var loc = url ? zerobin.parseUrl(url) : window.location; var loc = url ? zerobin.parseUrl(url) : window.location;
return loc.hash.replace('#', '').replace(/(\?|&).*$/, ''); return loc.hash.replace('#', '').replace(/(\?|&).*$/, '');
@ -541,7 +584,7 @@ window.zerobin = {
title: title, title: title,
content: message, content: message,
type: type, type: type,
action: action, action: action || {},
}); });
callback && callback() callback && callback()
}, },
@ -681,6 +724,7 @@ let content = '';
if (pasteContent) { if (pasteContent) {
content = pasteContent.textContent.trim(); content = pasteContent.textContent.trim();
app.currentPaste.id = zerobin.getPasteId(window.location);
} }
var key = zerobin.getPasteKey(); var key = zerobin.getPasteKey();
@ -788,6 +832,7 @@ window.onload = function () {
/* Display previous pastes */ /* Display previous pastes */
if (app.support.localStorage) { if (app.support.localStorage) {
app.previousPastes = zerobin.getPreviousPastes(); app.previousPastes = zerobin.getPreviousPastes();
app.currentPaste.ownerKey = localStorage.getItem('zerobinV' + zerobin.version + "#" + zerobin.getPasteId(window.location) + "#owner_key");
} }
/* Upload file using HTML5 File API */ /* Upload file using HTML5 File API */
@ -804,7 +849,7 @@ if (app.support.fileUpload) {
/* Remove expired pasted from history */ /* Remove expired pasted from history */
if (app.support.history && zerobin.paste_not_found) { if (app.support.history && zerobin.paste_not_found) {
var paste_id = zerobin.getPasteId(); var paste_id = zerobin.getPasteId();
var keys = zerobin.getLocalStorageKeys(); var keys = zerobin.getLocalStorageURLKeys();
keys.forEach((key, i) => { keys.forEach((key, i) => {
if (localStorage[key].indexOf(paste_id) !== -1) { if (localStorage[key].indexOf(paste_id) !== -1) {
localStorage.removeItem(key); localStorage.removeItem(key);

View File

@ -1,7 +1,3 @@
# coding: utf-8
from __future__ import print_function, unicode_literals, absolute_import
import time import time
import os import os
import glob import glob
@ -12,7 +8,6 @@ import unicodedata
from functools import partial from functools import partial
from zerobin import default_settings from zerobin import default_settings
sys.path.append(default_settings.LIBS_DIR)
try: try:
from zerobin.privilege import drop_privileges_permanently, coerce_user, coerce_group from zerobin.privilege import drop_privileges_permanently, coerce_user, coerce_group
@ -24,7 +19,7 @@ try:
except ImportError: except ImportError:
# python-2.6 or earlier - use simplier less-optimized execfile() # python-2.6 or earlier - use simplier less-optimized execfile()
def run_path(file_path): def run_path(file_path):
mod_globals = {'__file__': file_path} mod_globals = {"__file__": file_path}
execfile(file_path, mod_globals) execfile(file_path, mod_globals)
return mod_globals return mod_globals
@ -43,8 +38,7 @@ def drop_privileges(user=None, group=None, wait=5):
user = coerce_user(user) user = coerce_user(user)
group = coerce_group(group) group = coerce_group(group)
lock_files = glob.glob(os.path.join(tempfile.gettempdir(), lock_files = glob.glob(os.path.join(tempfile.gettempdir(), "bottle.*.lock"))
'bottle.*.lock'))
for lock_file in lock_files: for lock_file in lock_files:
os.chown(lock_file, user, group) os.chown(lock_file, user, group)
@ -78,12 +72,10 @@ class SettingsContainer(object):
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if not cls._instance: if not cls._instance:
cls._instance = super(SettingsContainer, cls).__new__(cls, *args, cls._instance = super(SettingsContainer, cls).__new__(cls, *args, **kwargs)
**kwargs)
cls._instance.update_with_module(default_settings) cls._instance.update_with_module(default_settings)
return cls._instance return cls._instance
def update_with_dict(self, dict): def update_with_dict(self, dict):
""" """
Update settings with values from the given mapping object. Update settings with values from the given mapping object.
@ -94,7 +86,6 @@ class SettingsContainer(object):
setattr(self, name, value) setattr(self, name, value)
return self return self
def update_with_module(self, module): def update_with_module(self, module):
""" """
Update settings with values from the given module. Update settings with values from the given module.
@ -102,7 +93,6 @@ class SettingsContainer(object):
""" """
return self.update_with_dict(module.__dict__) return self.update_with_dict(module.__dict__)
@classmethod @classmethod
def from_module(cls, module): def from_module(cls, module):
""" """
@ -113,7 +103,6 @@ class SettingsContainer(object):
settings.update_with_module(module) settings.update_with_module(module)
return settings return settings
def update_with_file(self, filepath): def update_with_file(self, filepath):
""" """
Update settings with values from the given module file. 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, Try to replace non ASCII char by similar ASCII char. If it can't,
replace it with "?". 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 # 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)"> <input type="file" class="hide-upload" id="file-upload" @change="handleUpload($event.target.files)">
</p> </p>
<form class="well" method="post" action="/paste/create"> <form class="well" method="post" action="/paste/create" @submit.prevent="encryptAndSendPaste()">
<p class="paste-option"> <p class=" paste-option">
<label for="expiration">Expiration:</label> <label for="expiration">Expiration:</label>
<select id="expiration" name="expiration" v-model="newPaste.expiration"> <select id="expiration" name="expiration" v-model="newPaste.expiration">
<option value="burn_after_reading">Burn after reading</option> <option value="burn_after_reading">Burn after reading</option>
@ -25,14 +25,14 @@
<option value="1_month">1 month</option> <option value="1_month">1 month</option>
<option value="never">Never</option> <option value="never">Never</option>
</select> </select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button> <button type="submit" class="btn btn-primary" ">Submit</button>
</p> </p>
<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 class="bar"></div>
</div> </div>
<textarea rows="10" style="width:100%;" class="input-xlarge" id="content" name="content" autofocus <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>
<p class="paste-option down" v-if="displayBottomToolBar"> <p class="paste-option down" v-if="displayBottomToolBar">
<label for="expiration">Expiration:</label> <label for="expiration">Expiration:</label>
@ -42,7 +42,7 @@
<option value="1_month">1 month</option> <option value="1_month">1 month</option>
<option value="never">Never</option> <option value="never">Never</option>
</select> </select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
</p> </p>
</form> </form>

View File

@ -12,11 +12,11 @@
<link rel="shortcut icon" href="/favicon.ico"> <link rel="shortcut icon" href="/favicon.ico">
%if settings.COMPRESSED_STATIC_FILES: %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: %else:
<link href="/static/css/prettify.css" rel="stylesheet" /> <link href="/static/css/prettify.css" rel="stylesheet" />
<link href="/static/css/bootstrap.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 %end
<!-- Le HTML5 shim, for IE7-8 support of HTML5 elements --> <!-- 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"> <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> <strong class="title" v-if="msg.title" v-html="msg.title"></strong>
<span class="message" v-html="msg.content"></span> <span class="message" v-html="msg.content"></span>
<a v-if="msg.action.message" href="#" <a v-if="msg.action.message" href="#"
@ -156,10 +156,10 @@
<script src="/static/js/vue.js"></script> <script src="/static/js/vue.js"></script>
%if settings.COMPRESSED_STATIC_FILES: %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: %else:
<script src="/static/js/sjcl.js"></script> <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 %end
<script type="text/javascript"> <script type="text/javascript">
@ -168,7 +168,7 @@
</script> </script>
%if settings.COMPRESSED_STATIC_FILES: %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: %else:
<script src="/static/js/lzw.js"></script> <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)"> <input type="file" class="hide-upload" id="file-upload" @change="handleUpload($event.target.files)">
</p> </p>
<form class="well" method="post" action="/paste/create"> <form class="well" method="post" action="/paste/create" @submit.prevent="encryptAndSendPaste()">
<p class="paste-option"> <p class="paste-option">
<label for="expiration">Expiration:</label> <label for="expiration">Expiration:</label>
<select id="expiration" name="expiration" v-model="newPaste.expiration"> <select id="expiration" name="expiration" v-model="newPaste.expiration">
@ -13,14 +13,14 @@
<option value="1_month">1 month</option> <option value="1_month">1 month</option>
<option value="never">Never</option> <option value="never">Never</option>
</select> </select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
</p> </p>
<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 class="bar"></div>
</div> </div>
<textarea rows="10" style="width:100%" class="input-xlarge" id="content" name="content" autofocus <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>
<p class="paste-option down" v-if="displayBottomToolBar"> <p class="paste-option down" v-if="displayBottomToolBar">
@ -31,7 +31,7 @@
<option value="1_month">1 month</option> <option value="1_month">1 month</option>
<option value="never">Never</option> <option value="never">Never</option>
</select> </select>
<button type="submit" class="btn btn-primary" @click="encryptAndSendPaste($event)">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
</p> </p>
</form> </form>

View File

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