E-book viewer: Change the file format used to import/export bookmarks to use JSON. This prevents malicious bookmarks files from causing code execution.

Also more work on the EM page for the server.
This commit is contained in:
Kovid Goyal 2018-03-07 10:05:56 +05:30
parent 706c3ba805
commit aeb5b036a0
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 87 additions and 15 deletions

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import cPickle
import json
from PyQt5.Qt import (
Qt, QListWidget, QListWidgetItem, QItemSelectionModel, QAction,
@ -186,10 +186,10 @@ class BookmarkManager(QWidget):
self.edited.emit(bm)
def bm_to_item(self, bm):
return bytearray(cPickle.dumps(bm, -1))
return bm.copy()
def item_to_bm(self, item):
return cPickle.loads(bytes(item.data(Qt.UserRole)))
return item.data(Qt.UserRole).copy()
def get_bookmarks(self):
return list(self)
@ -197,21 +197,21 @@ class BookmarkManager(QWidget):
def export_bookmarks(self):
filename = choose_save_file(
self, 'export-viewer-bookmarks', _('Export bookmarks'),
filters=[(_('Saved bookmarks'), ['pickle'])], all_files=False, initial_filename='bookmarks.pickle')
filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, initial_filename='bookmarks.calibre-bookmarks')
if filename:
with open(filename, 'wb') as fileobj:
cPickle.dump(self.get_bookmarks(), fileobj, -1)
with lopen(filename, 'wb') as fileobj:
fileobj.write(json.dumps(self.get_bookmarks(), indent=True))
def import_bookmarks(self):
files = choose_files(self, 'export-viewer-bookmarks', _('Import bookmarks'),
filters=[(_('Saved bookmarks'), ['pickle'])], all_files=False, select_only_single_file=True)
filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, select_only_single_file=True)
if not files:
return
filename = files[0]
imported = None
with open(filename, 'rb') as fileobj:
imported = cPickle.load(fileobj)
with lopen(filename, 'rb') as fileobj:
imported = json.load(fileobj)
if imported is not None:
bad = False

View File

@ -26,7 +26,7 @@ from calibre.srv.metadata import (
from calibre.srv.routes import endpoint, json
from calibre.srv.utils import get_library_data, get_use_roman
from calibre.utils.config import prefs, tweaks
from calibre.utils.icu import sort_key
from calibre.utils.icu import sort_key, numeric_sort_key
from calibre.utils.localization import get_lang
from calibre.utils.search_query_parser import ParseException
@ -393,4 +393,4 @@ def field_names(ctx, rd, field):
Optional: ?library_id=<default library>
'''
db, library_id = get_library_data(ctx, rd)[:2]
return tuple(db.all_field_names(field))
return tuple(sorted(db.all_field_names(field), key=numeric_sort_key))

View File

@ -15,6 +15,7 @@ from book_list.library_data import (
loaded_book_ids, set_book_metadata
)
from book_list.router import back
from book_list.theme import get_color
from book_list.top_bar import create_top_bar, set_title
from book_list.ui import set_panel_handler, show_panel
from date import format_date
@ -39,6 +40,11 @@ add_extra_css(def():
style += build_rule(sel + 'table.metadata td', padding_bottom='0.5ex', padding_top='0.5ex', cursor='pointer')
style += build_rule(sel + 'table.metadata tr:hover', color='red')
style += build_rule(sel + 'table.metadata tr:active', transform='scale(1.5)')
style += build_rule(sel + '.completions', display='flex', flex_wrap='wrap', align_items='center')
style += build_rule(sel + '.completions > div', margin='0.5ex 0.5rem', margin_left='0', padding='0.5ex 0.5rem', border='solid 1px currentColor', border_radius='1ex', cursor='pointer')
style += build_rule(sel + '.completions > div:active', transform='scale(1.5)')
style += build_rule(sel + '.completions > div:hover', background=get_color('window-foreground'), color=get_color('window-background'))
return style
)
@ -114,21 +120,83 @@ def simple_line_edit(container_id, book_id, field, fm, div, mi):
return x
def add_completion(container_id, name):
pass
def show_completions(container_id, div, field, prefix, names):
clear(div)
completions = E.div(class_='completions')
div.appendChild(completions)
for i, name in enumerate(names):
completions.appendChild(E.div(name, onclick=add_completion.bind(None, container_id, name)))
if i >= 50:
break
def update_completions(container_id, ok, field, names):
c = document.getElementById(container_id)
if not c:
return
d = c.querySelector('div[data-ctype="edit"]')
if not d or d.style.display is not 'block':
return
div = d.lastChild
clear(div)
if not ok:
err = E.div()
safe_set_inner_html(err, names)
div.appendChild(E.div(
_('Failed to download items for completion, with error:'), err
))
return
val = d.querySelector('input').value or ''
val = value_to_json(val)
if jstype(val) is 'string':
prefix = val
else:
prefix = val[-1] if val.length else ''
if prefix is update_completions.prefix:
return
pl = prefix.toLowerCase().strip()
if pl:
if pl.startswith(update_completions.prefix.toLowerCase()):
matching_names = [x for x in update_completions.names if x.toLowerCase().startswith(pl)]
else:
matching_names = [x for x in names if x.toLowerCase().startswith(pl)]
else:
matching_names = []
update_completions.prefix = prefix
update_completions.names = matching_names
show_completions(container_id, div, field, prefix, matching_names)
update_completions.ui_to_list = None
update_completions.list_to_ui = None
update_completions.names = v'[]'
update_completions.prefix = ''
def line_edit_updated(container_id, field):
field_names_for(field, update_completions.bind(None, container_id))
def multiple_line_edit(list_to_ui, ui_to_list, container_id, book_id, field, fm, div, mi):
nonlocal value_to_json
update_completions.ui_to_list = ui_to_list
update_completions.list_to_ui = list_to_ui
name = fm.name or field
le = E.input(type='text', name=name.replace('#', '_c_'), autocomplete=True)
le = E.input(type='text', name=name.replace('#', '_c_'), autocomplete=True, oninput=line_edit_updated.bind(None, container_id, field))
le.value = (resolved_metadata(mi, field) or v'[]').join(list_to_ui)
form = create_form(le, line_edit_get_value, container_id, book_id, field)
div.appendChild(E.div(style='margin: 0.5ex 1rem', _(
'Edit the "{0}" below. Multiple items can be separated by {1}.').format(name, list_to_ui.strip())))
div.appendChild(E.div(style='margin: 0.5ex 1rem', form))
div.appendChild(E.div(style='margin: 0.5ex 1rem'))
div.appendChild(E.div(E.span(_('Loading all {}...').format(name)), style='margin: 0.5ex 1rem'))
le.focus(), le.select()
value_to_json = def(x):
return [a.strip() for a in x.split(ui_to_list) if a.strip()]
div.lastChild.appendChild(E.span(_('Loading all {}...').format(name)))
field_names_for(field, print)
field_names_for(field, update_completions.bind(None, container_id))
def edit_field(container_id, book_id, field):
@ -142,6 +210,10 @@ def edit_field(container_id, book_id, field):
d.style.display = 'block'
d.previousSibling.style.display = 'none'
clear(d)
update_completions.ui_to_list = None
update_completions.list_to_ui = None
update_completions.names = v'[]'
update_completions.prefix = ''
if field is 'authors':
multiple_line_edit(' & ', '&', container_id, book_id, field, fm, d, mi)
else: