Commit f62d6abb authored by Ozzie Isaacs's avatar Ozzie Isaacs

Merge branch 'master' into development

# Conflicts:
#	cps/server.py
#	test/Calibre-Web TestSummary_Linux.html
parents 10942527 12ad7a63
......@@ -22,7 +22,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Support for public user registration
- Send eBooks to Kindle devices with the click of a button
- Sync your Kobo devices through Calibre-Web with your Calibre library
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
- Support for Calibre Custom Columns
- Ability to hide content based on categories and Custom Column content per user
......
......@@ -37,6 +37,7 @@ try:
except ImportError:
from flask_login.__about__ import __version__ as flask_loginVersion
try:
# pylint: disable=unused-import
import unidecode
# _() necessary to make babel aware of string for translation
unidecode_version = _(u'installed')
......
......@@ -49,7 +49,7 @@ def init_cache_busting(app):
# compute version component
rooted_filename = os.path.join(dirpath, filename)
with open(rooted_filename, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
......@@ -64,6 +64,7 @@ def init_cache_busting(app):
return filename.split("?", 1)[0]
@app.url_defaults
# pylint: disable=unused-variable
def reverse_to_cache_busted_url(endpoint, values):
"""
Make `url_for` produce busted filenames when using the 'static' endpoint.
......
......@@ -126,7 +126,7 @@ LDAP_AUTH_SIMPLE = 0
DEFAULT_MAIL_SERVER = "mail.example.org"
DEFAULT_PASSWORD = "admin123" # nosec # noqa
DEFAULT_PASSWORD = "admin123" # nosec
DEFAULT_PORT = 8083
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
try:
......
......@@ -211,31 +211,8 @@ def delete_book_from_details(book_id):
def delete_book_ajax(book_id, book_format):
return delete_book(book_id,book_format, False)
def delete_book(book_id, book_format, jsonResponse):
warning = {}
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
"format": "",
"error": error}),
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
"type": "warning",
"format": "",
"error": error}
else:
flash(error, category="warning")
if not book_format:
def delete_whole_book(book_id, book):
# delete book from Shelfs, Downloads, Read list
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
......@@ -250,7 +227,7 @@ def delete_book(book_id, book_format, jsonResponse):
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers')
cc = calibre_db.session.query(db.Custom_Columns).\
cc = calibre_db.session.query(db.Custom_Columns). \
filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
......@@ -279,16 +256,9 @@ def delete_book(book_id, book_format, jsonResponse):
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
calibre_db.session, 'custom')
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete()
calibre_db.session.commit()
except Exception as e:
log.debug_or_exception(e)
calibre_db.session.rollback()
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
def render_delete_book_result(book_format, jsonResponse, warning, book_id):
if book_format:
if jsonResponse:
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
......@@ -309,6 +279,45 @@ def delete_book(book_id, book_format, jsonResponse):
return redirect(url_for('web.index'))
def delete_book(book_id, book_format, jsonResponse):
warning = {}
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
"format": "",
"error": error}),
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
"type": "warning",
"format": "",
"error": error}
else:
flash(error, category="warning")
if not book_format:
delete_whole_book(book_id, book)
else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete()
calibre_db.session.commit()
except Exception as e:
log.debug_or_exception(e)
calibre_db.session.rollback()
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
def render_edit_book(book_id):
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
......@@ -447,18 +456,8 @@ def edit_book_publisher(to_save, book):
return changed
def edit_cc_data(book_id, book, to_save):
def edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string):
changed = False
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
if len(getattr(book, cc_string)) > 0:
cc_db_value = getattr(book, cc_string)[0].value
else:
cc_db_value = None
if to_save[cc_string].strip():
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
if to_save[cc_string] == 'None':
to_save[cc_string] = None
elif c.datatype == 'bool':
......@@ -479,8 +478,11 @@ def edit_cc_data(book_id, book, to_save):
new_cc = cc_class(value=to_save[cc_string], book=book_id)
calibre_db.session.add(new_cc)
changed = True
return changed, to_save
else:
def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
changed = False
if c.datatype == 'rating':
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
if to_save[cc_string].strip() != cc_db_value:
......@@ -504,6 +506,24 @@ def edit_cc_data(book_id, book, to_save):
cc_class.value == to_save[cc_string].strip()).first()
# add cc value to book
getattr(book, cc_string).append(new_cc)
return changed, to_save
def edit_cc_data(book_id, book, to_save):
changed = False
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
if len(getattr(book, cc_string)) > 0:
cc_db_value = getattr(book, cc_string)[0].value
else:
cc_db_value = None
if to_save[cc_string].strip():
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
changed, to_save = edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string)
else:
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
else:
if cc_db_value is not None:
# remove old cc_val
......@@ -766,6 +786,7 @@ def merge_metadata(to_save, meta):
to_save["description"] = to_save["description"] or Markup(
getattr(meta, 'description', '')).unescape()
def identifier_list(to_save, book):
"""Generate a list of Identifiers from form information"""
id_type_prefix = 'identifier-type-'
......@@ -780,43 +801,8 @@ def identifier_list(to_save, book):
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
return result
@editbook.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano
@upload_required
def upload():
if not config.config_uploading:
abort(404)
if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"):
try:
modif_date = False
# create the function for sorting...
calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
# check if file extension is correct
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
title = meta.title
authr = meta.author
def prepare_authors_on_upload(title, authr):
if title != _(u'Unknown') and authr != _(u'Unknown'):
entry = calibre_db.check_exists_book(authr, title)
if entry:
......@@ -835,7 +821,7 @@ def upload():
if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author
sort_authors_list=list()
sort_authors_list = list()
db_author = None
for inp in input_authors:
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
......@@ -851,12 +837,20 @@ def upload():
sort_author = stored_author.sort
sort_authors_list.append(sort_author)
sort_authors = ' & '.join(sort_authors_list)
return sort_authors, input_authors, db_author
def create_book_on_upload(modif_date, meta):
title = meta.title
authr = meta.author
sort_authors, input_authors, db_author = prepare_authors_on_upload(title, authr)
title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(db_author.name)
# combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/')
# Calibre adds books with utc as timezone
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
......@@ -884,6 +878,44 @@ def upload():
# flush content, get db_book.id available
calibre_db.session.flush()
return db_book, input_authors, title_dir
@editbook.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano
@upload_required
def upload():
if not config.config_uploading:
abort(404)
if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"):
try:
modif_date = False
# create the function for sorting...
calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
# check if file extension is correct
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta)
# Comments needs book id therfore only possible after flush
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
......
......@@ -60,14 +60,8 @@ def init_errorhandler():
if services.ldap:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
# pylint: disable=unused-variable
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
# @app.errorhandler(InvalidRequestError)
#@app.errorhandler(OperationalError)
#def handle_db_exception(e):
# db.session.rollback()
# log.error('Database request error: %s',e)
# return internal_error(InternalServerError(e))
......@@ -142,7 +142,7 @@ def on_received_watch_confirmation():
else:
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
......
......@@ -187,7 +187,7 @@ def check_send_to_kindle(entry):
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
# list with supported formats
def check_read_formats(entry):
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'}
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
bookformats = list()
if len(entry.data):
for ele in iter(entry.data):
......@@ -730,7 +730,7 @@ def format_runtime(runtime):
# helper function to apply localize status information in tasklist entries
def render_task_status(tasklist):
renderedtasklist = list()
for __, user, added, task in tasklist:
for __, user, __, task in tasklist:
if user == current_user.nickname or current_user.role_admin():
ret = {}
if task.start_time:
......
......@@ -918,7 +918,7 @@ def HandleAuthRequest():
if config.config_kobo_proxy:
try:
return redirect_or_proxy_request()
except:
except Exception:
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
return make_calibre_web_auth_response()
......
......@@ -81,6 +81,7 @@ log = logger.create()
def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor
# pylint: disable=unused-variable
def pop_auth_token(__, values):
g.auth_token = values.pop("auth_token")
......
......@@ -96,86 +96,7 @@ def logout_oauth_user():
session.pop(str(oauth_key) + '_oauth_user_id')
if ub.oauth_support:
oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider()
oauthProvider.provider_name = provider
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session_commit("{} Blueprint Created".format(provider))
oauth_ids = ub.session.query(ub.OAuthProvider).all()
ele1 = dict(provider_name='github',
id=oauth_ids[0].id,
active=oauth_ids[0].active,
oauth_client_id=oauth_ids[0].oauth_client_id,
scope=None,
oauth_client_secret=oauth_ids[0].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele2 = dict(provider_name='google',
id=oauth_ids[1].id,
active=oauth_ids[1].active,
scope=["https://www.googleapis.com/auth/userinfo.email"],
oauth_client_id=oauth_ids[1].oauth_client_id,
oauth_client_secret=oauth_ids[1].oauth_client_secret,
obtain_link='https://console.developers.google.com/apis/credentials')
oauthblueprints.append(ele1)
oauthblueprints.append(ele2)
for element in oauthblueprints:
if element['provider_name'] == 'github':
blueprint_func = make_github_blueprint
else:
blueprint_func = make_google_blueprint
blueprint = blueprint_func(
client_id=element['oauth_client_id'],
client_secret=element['oauth_client_secret'],
redirect_to="oauth."+element['provider_name']+"_login",
scope=element['scope']
)
element['blueprint'] = blueprint
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
user=current_user, user_required=True)
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
return False
github_info = resp.json()
github_user_id = str(github_info["id"])
return oauth_update_token(str(oauthblueprints[0]['id']), token, github_user_id)
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
return False
google_info = resp.json()
google_user_id = str(google_info["id"])
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
def oauth_update_token(provider_id, token, provider_user_id):
def oauth_update_token(provider_id, token, provider_user_id):
session[provider_id + "_oauth_user_id"] = provider_user_id
session[provider_id + "_oauth_token"] = token
......@@ -202,7 +123,7 @@ if ub.oauth_support:
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
......@@ -242,7 +163,7 @@ if ub.oauth_support:
return redirect(url_for(redirect_url))
def get_oauth_status():
def get_oauth_status():
status = []
query = ub.session.query(ub.OAuth).filter_by(
user_id=current_user.id,
......@@ -256,7 +177,7 @@ if ub.oauth_support:
return None
def unlink_oauth(provider):
def unlink_oauth(provider):
if request.host_url + 'me' != request.referrer:
pass
query = ub.session.query(ub.OAuth).filter_by(
......@@ -281,6 +202,88 @@ if ub.oauth_support:
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
def generate_oauth_blueprints():
oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider()
oauthProvider.provider_name = provider
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session_commit("{} Blueprint Created".format(provider))
oauth_ids = ub.session.query(ub.OAuthProvider).all()
ele1 = dict(provider_name='github',
id=oauth_ids[0].id,
active=oauth_ids[0].active,
oauth_client_id=oauth_ids[0].oauth_client_id,
scope=None,
oauth_client_secret=oauth_ids[0].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele2 = dict(provider_name='google',
id=oauth_ids[1].id,
active=oauth_ids[1].active,
scope=["https://www.googleapis.com/auth/userinfo.email"],
oauth_client_id=oauth_ids[1].oauth_client_id,
oauth_client_secret=oauth_ids[1].oauth_client_secret,
obtain_link='https://console.developers.google.com/apis/credentials')
oauthblueprints.append(ele1)
oauthblueprints.append(ele2)
for element in oauthblueprints:
if element['provider_name'] == 'github':
blueprint_func = make_github_blueprint
else:
blueprint_func = make_google_blueprint
blueprint = blueprint_func(
client_id=element['oauth_client_id'],
client_secret=element['oauth_client_secret'],
redirect_to="oauth."+element['provider_name']+"_login",
scope=element['scope']
)
element['blueprint'] = blueprint
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
user=current_user, user_required=True)
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
return oauthblueprints
if ub.oauth_support:
oauthblueprints = generate_oauth_blueprints()
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
return False
github_info = resp.json()
github_user_id = str(github_info["id"])
return oauth_update_token(str(oauthblueprints[0]['id']), token, github_user_id)
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
return False
google_info = resp.json()
google_user_id = str(google_info["id"])
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
# notify on OAuth provider error
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
......
......@@ -22,7 +22,7 @@ import os
import errno
import signal
import socket
import subprocess
import subprocess # nosec
try:
from gevent.pywsgi import WSGIServer
......@@ -260,7 +260,7 @@ class WebServer(object):
log.info("Performing restart of Calibre-Web")
args = self._get_args_for_reloading()
subprocess.call(args, close_fds=True)
subprocess.call(args, close_fds=True) # nosec
return True
def _killServer(self, __, ___):
......
......@@ -22,6 +22,7 @@ from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
from datetime import datetime
try:
# pylint: disable=unused-import
from urllib import unquote
except ImportError:
from urllib.parse import unquote
......@@ -91,14 +92,14 @@ class SyncToken:
def __init__(
self,
raw_kobo_store_token="", # nosec
raw_kobo_store_token="",
books_last_created=datetime.min,
books_last_modified=datetime.min,
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min,
books_last_id=-1
):
): # nosec
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
self.books_last_modified = books_last_modified
......
body {
margin: 0px;
}
#djvuContainer {
position: absolute;
width: 100%;
height: 100%;
max-width: 100%;
text-align: center;
overflow: hidden;
}
.toolbar {
position: relative;
display: inline-block;
padding-top: 10px;
transform: translate(0, 0);
-webkit-transform: translate(0, 0);
-ms-transform: translate(0, 0);
transition: transform 0.3s;
-webkit-transition: -webkit-transform 0.3s;
}
.toolbarHidden {
transform: translate(0, -100%);
-webkit-transform: translate(0, -100%);
-ms-transform: translate(0, -100%);
transition: transform 1s;
-webkit-transition: transform 1s;
}
.toolbarSquareButton {
float: left;
width: 40px;
height: 40px;
background-image: url("img/toolbar-buttons.png");
background-repeat: no-repeat;
background-size: 500% 300%;
}
.scrollbar {
position: absolute;
border-radius: 6px;
opacity: 0.6;
box-shadow: inset 0 0 0 1px black, inset 0 0 0 2px white, inset 0 0 0 10px #BBB;
transition: opacity 0.3s;
}
.scrollbar:hover {
box-shadow: inset 0 0 0 1px black, inset 0 0 0 2px white, inset 0 0 0 10px #999;
}
.scrollbarClicked, .scrollbarClicked:hover {
box-shadow: inset 0 0 0 1px black, inset 0 0 0 2px white, inset 0 0 0 10px #777;
}
.scrollbarHidden {
opacity: 0;
transition: opacity 0.6s;
}
.scrollbarVertical {
right: 0px;
border-right: 1px solid transparent;
width: 13px;
}
.scrollbarHorizontal {
bottom: 0px;
border-bottom: 1px solid transparent;
height: 13px;
}
.content {
overflow: hidden;
position: absolute;
height: 100%;
width: 100%;
}
.textLayer {
position: absolute;
height: 120%;
width: 120%;
overflow: scroll;
text-align: left;
}
_:-ms-lang(x), .textLayer {
height: 100%;
width: 100%;
-ms-overflow-style: none;
}
.textPage {
margin-top: 100vh;
margin-bottom: 100vh;
padding-right: 100vw;
}
.textPage span {
font-family: sans-serif;
color: #000;
color: rgba(0, 0, 0, 0);
white-space: nowrap;
}
.visibleTextPage span {
display: inline-block;
position: relative;
top: 50%;
transform: translateY(-50%);
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
/* border: 1px solid red; /* for easy debug */
}
.buttonZoomIn {
background-position: 25% 0;
}
.buttonZoomIn:hover {
background-position: 25% 50%;
}
.buttonZoomIn:disabled {
background-position: 25% 100%;
}
.buttonZoomOut {
background-position: 0 0;
}
.buttonZoomOut:hover {
background-position: 0 50%;
}
.buttonZoomOut:disabled {
background-position: 0 100%;
}
.buttonPagePrev {
background-position: 50% 0;
}
.buttonPagePrev:hover {
background-position: 50% 50%;
}
.buttonPagePrev:disabled {
background-position: 50% 100%;
}
.buttonPageNext {
background-position: 75% 0;
}
.buttonPageNext:hover {
background-position: 75% 50%;
}
.buttonPageNext:disabled {
background-position: 75% 100%;
}
.toolbarItem {
display: inline-block;
margin: 0 10px;
}
.comboBox {
float: left;
position: relative;
}
.comboBoxSelection {
width: 8.25ex;
margin: 10px 12px 0px 12px;
}
.comboBoxText {
width: 5ex;
border: none;
padding: 0px;
outline: none;
position: absolute;
margin: 10px 0px 0px 12px;
top: 2px;
left: 3px;
}
.statusImage {
position: absolute;
left: 50%;
top: 50%;
width: 128px;
height: 128px;
margin: -72px 0 0 -64px;
background-image: url("img/status.png");
background-repeat: no-repeat;
}
.blankImage {
background-image: url("img/blank.jpg");
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
091ECB3AE852C68866FBC86AA8FCDB1F.cache.js
user.agent ie8
1A420474460884E73E3288F783AD7024.cache.js
user.agent ie10
3CFDA9D9AFA2798299BBAE243DB2E9B5.cache.js
user.agent safari
564CB406D925C79CCD67EB98CA5AD4EF.cache.js
user.agent gecko1_8
A8305F17E026239876FCBC730B035A55.cache.js
user.agent ie9
Devmode:devmode.js
\ No newline at end of file
djvu_html5.onScriptDownloaded(['var $wnd = $wnd || window.parent;var __gwtModuleFunction = $wnd.djvu_html5;var $sendStats = __gwtModuleFunction.__sendStats;$sendStats(\'moduleStartup\', \'moduleEvalStart\');var $gwt_version = "2.8.1";var $strongName = \'\';var $gwt = {};var $doc = $wnd.document;var $moduleName, $moduleBase;function __gwtStartLoadingFragment(frag) {var fragFile = \'deferredjs/\' + $strongName + \'/\' + frag + \'.cache.js\';return __gwtModuleFunction.__startLoadingFragment(fragFile);}function __gwtInstallCode(code) {return __gwtModuleFunction.__installRunAsyncCode(code);}function __gwt_isKnownPropertyValue(propName, propValue) {return __gwtModuleFunction.__gwt_isKnownPropertyValue(propName, propValue);}function __gwt_getMetaProperty(name) {return __gwtModuleFunction.__gwt_getMetaProperty(name);}var $stats = $wnd.__gwtStatsEvent ? function(a) {return $wnd.__gwtStatsEvent && $wnd.__gwtStatsEvent(a);} : null;var $sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null;/******************************************************************************\n * Variables used by the Plugin\n *****************************************************************************/\nvar $entry;\nvar $hostedHtmlVersion="2.1";\n\nvar $errFn;\nvar $moduleName;\nvar $moduleBase;\nvar __gwt_getProperty;\n\n/******************************************************************************\n * WRITE ME - what does this invokes stuff do??? Probably related to invoking\n * calls...\n *****************************************************************************/\nvar __gwt_javaInvokes = [];\n\n// Wrapper to call JS methods, which we need both to be able to supply a\n// different this for method lookup and to get the exception back\nfunction __gwt_jsInvoke(thisObj, methodName) {\n try {\n var args = Array.prototype.slice.call(arguments, 2);\n return [0, window[methodName].apply(thisObj, args)];\n } catch (e) {\n return [1, e];\n }\n}\n\nfunction __gwt_makeJavaInvoke(argCount) {\n return __gwt_javaInvokes[argCount] || __gwt_doMakeJavaInvoke(argCount);\n}\n\nfunction __gwt_doMakeJavaInvoke(argCount) {\n // IE6 won\'t eval() anonymous functions except as r-values\n var argList = "";\n for (var i = 0; i < argCount; i++) {\n argList += ",p" + i;\n }\n var argListNoComma = argList.substring(1);\n\n return eval(\n "__gwt_javaInvokes[" + argCount + "] =\\n" +\n " function(thisObj, dispId" + argList + ") {\\n" +\n " var result = __static(dispId, thisObj" + argList + ");\\n" +\n " if (result[0]) {\\n" +\n " throw result[1];\\n" +\n " } else {\\n" +\n " return result[1];\\n" +\n " }\\n" +\n " }\\n"\n ); \n}\n\n\n/******************************************************************************\n * Functions used to create tear-offs of Java methods. Each function corresponds\n * to exactly one dispId, and also embeds the argument count. We get the "this"\n * value from the context in which the function is being executed.\n * Function-object identity is preserved by caching in a sparse array.\n *****************************************************************************/\nvar __gwt_tearOffs = [];\nvar __gwt_tearOffGenerators = [];\nfunction __gwt_makeTearOff(proxy, dispId, argCount) {\n return __gwt_tearOffs[dispId] || __gwt_doMakeTearOff(dispId, argCount);\n}\n\nfunction __gwt_doMakeTearOff(dispId, argCount) {\n return __gwt_tearOffs[dispId] = \n (__gwt_tearOffGenerators[argCount] || __gwt_doMakeTearOffGenerator(argCount))(dispId);\n}\n\nfunction __gwt_doMakeTearOffGenerator(argCount) {\n // IE6 won\'t eval() anonymous functions except as r-values\n var argList = "";\n for (var i = 0; i < argCount; i++) {\n argList += ",p" + i;\n }\n var argListNoComma = argList.substring(1);\n\n return eval(\n "__gwt_tearOffGenerators[" + argCount + "] =\\n" +\n " function(dispId) {\\n" +\n " return function(" + argListNoComma + ") {\\n" +\n " var result = __static(dispId, this" + argList + ");\\n" +\n " if (result[0]) {\\n" +\n " throw result[1];\\n" +\n " } else {\\n" +\n " return result[1];\\n" +\n " }\\n" +\n " }\\n" +\n " }\\n"\n ); \n}\n\n\n/******************************************************************************\n *Code to give visual feedback when something goes wrong in Dev Mode\n *****************************************************************************/\nfunction __gwt_disconnected() {\n // Prevent double-invocation.\n window.__gwt_disconnected = new Function();\n // Do it in a timeout so we can be sure we have a clean stack.\n window.setTimeout(__gwt_disconnected_impl, 1);\n}\n\nfunction __gwt_disconnected_impl() {\n __gwt_displayGlassMessage(\'GWT Code Server Disconnected\',\n \'Most likely, you closed GWT Development Mode. Or, you might have lost \'\n + \'network connectivity. To fix this, try restarting GWT Development Mode and \'\n + \'refresh this page.\');\n}\n\n// Keep track of z-index to allow layering of multiple glass messages\nvar __gwt_glassMessageZIndex = 2147483647;\n\n// Note this method is also used by ModuleSpace.java\nfunction __gwt_displayGlassMessage(summary, details) {\n var topWin = window.top;\n var topDoc = topWin.document;\n var outer = topDoc.createElement("div");\n // Do not insert whitespace or outer.firstChild will get a text node.\n outer.innerHTML = \n \'<div style="position:absolute;z-index:\' + __gwt_glassMessageZIndex-- +\n \';left:50px;top:50px;width:600px;color:#FFF;font-family:verdana;text-align:left;">\' +\n \'<div style="font-size:30px;font-weight:bold;">\' + summary + \'<\/div>\' +\n \'<div style="font-size:15px;">\' + details + \'<\/div>\' +\n \'<\/div>\' +\n \'<div style="position:absolute;z-index:\' + __gwt_glassMessageZIndex-- +\n \';left:0px;top:0px;right:0px;bottom:0px;filter:alpha(opacity=60);opacity:0.6;background-color:#000;"><\/div>\'\n ;\n topDoc.body.appendChild(outer);\n var glass = outer.firstChild;\n var glassStyle = glass.style;\n\n // Scroll to the top and remove scrollbars.\n topWin.scrollTo(0, 0);\n if (topDoc.compatMode == "BackCompat") {\n topDoc.body.style["overflow"] = "hidden";\n } else {\n topDoc.documentElement.style["overflow"] = "hidden";\n }\n\n // Steal focus.\n glass.focus();\n\n if ((navigator.userAgent.indexOf("MSIE") >= 0) && (topDoc.compatMode == "BackCompat")) {\n // IE quirks mode doesn\'t support right/bottom, but does support this.\n glassStyle.width = "125%";\n glassStyle.height = "100%";\n } else if (navigator.userAgent.indexOf("MSIE 6") >= 0) {\n // IE6 doesn\'t have a real standards mode, so we have to use hacks.\n glassStyle.width = "125%"; // Get past scroll bar area.\n // Nasty CSS; onresize would be better but the outer window won\'t let us add a listener IE.\n glassStyle.setExpression("height", "document.documentElement.clientHeight");\n }\n\n $doc.title = summary + " [" + $doc.title + "]";\n}\n\n\n/******************************************************************************\n * Other functions called by the Plugin\n *****************************************************************************/\nfunction __gwt_makeResult(isException, result) {\n return [isException, result];\n}\n\n//should be prefixed with "__gwt_"\nfunction fireOnModuleLoadStart(className) {\n $sendStats("moduleStartup", "onModuleLoadStart");\n}\n\n\n/******************************************************************************\n * Helper functions for the Development Mode startup code. Listed alphabetically\n *****************************************************************************/\nfunction doBrowserSpecificFixes() {\n var ua = navigator.userAgent.toLowerCase();\n if (ua.indexOf("gecko") != -1) {\n // install eval wrapper on FF to avoid EvalError problem\n var __eval = window.eval;\n window.eval = function(s) {\n return __eval(s);\n }\n }\n if (ua.indexOf("chrome") != -1) {\n // work around __gwt_ObjectId appearing in JS objects\n var hop = window.Object.prototype.hasOwnProperty;\n window.Object.prototype.hasOwnProperty = function(prop) {\n return prop != "__gwt_ObjectId" && hop.call(this, prop);\n };\n var hop2 = window.Object.prototype.propertyIsEnumerable;\n window.Object.prototype.propertyIsEnumerable = function(prop) {\n return prop != "__gwt_ObjectId" && hop2.call(this, prop);\n };\n // do the same in the main window if it is different from our window\n if ($wnd != window) {\n var hop3 = $wnd.Object.prototype.hasOwnProperty;\n $wnd.Object.prototype.hasOwnProperty = function(prop) {\n return prop != "__gwt_ObjectId" && hop3.call(this, prop);\n };\n var hop4 = $wnd.Object.prototype.propertyIsEnumerable;\n $wnd.Object.prototype.propertyIsEnumerable = function(prop) {\n return prop != "__gwt_ObjectId" && hop4.call(this, prop);\n };\n }\n }\n}\n\nfunction embedPlugin() {\n var embed = document.createElement(\'embed\');\n embed.id = \'pluginEmbed\';\n embed.type = \'application/x-gwt-hosted-mode\';\n embed.width = \'10\';\n embed.height = \'20\';\n\n var obj = document.createElement(\'object\');\n obj.id = \'pluginObject\';\n obj.classid = \'clsid:1D6156B6-002B-49E7-B5CA-C138FB843B4E\';\n\n document.body.appendChild(embed);\n document.body.appendChild(obj);\n}\n\nfunction findPluginObject() {\n try {\n return document.getElementById(\'pluginObject\');\n } catch (e) {\n return null;\n }\n}\n\nfunction findPluginEmbed() {\n try {\n return document.getElementById(\'pluginEmbed\')\n } catch (e) {\n return null;\n }\n}\n\nfunction findPluginXPCOM() {\n try {\n return __gwt_HostedModePlugin;\n } catch (e) {\n return null;\n }\n}\n\nfunction getCodeServer() {\n var server = "localhost:9997";\n var query = $wnd.location.search;\n var idx = query.indexOf("gwt.codesvr=");\n if (idx >= 0) {\n idx += 12; // "gwt.codesvr=".length == 12\n } else {\n idx = query.indexOf("gwt.codesvr.djvu_html5=");\n idx += (13 + "djvu_html5".length); // \n }\n if (idx >= 0) {\n var amp = query.indexOf("&", idx);\n if (amp >= 0) {\n server = query.substring(idx, amp);\n } else {\n server = query.substring(idx);\n }\n // According to RFC 3986, some of this component\'s characters (e.g., \':\')\n // are reserved and *may* be escaped.\n return decodeURIComponent(server);\n }\n}\n\nfunction generateSessionId() {\n var ASCII_EXCLAMATION = 33;\n var ASCII_TILDE = 126;\n var chars = [];\n for (var i = 0; i < 16; ++i) {\n chars.push(Math.floor(ASCII_EXCLAMATION\n + Math.random() * (ASCII_TILDE - ASCII_EXCLAMATION + 1)));\n }\n return String.fromCharCode.apply(null, chars);\n}\n\nfunction loadIframe(url) {\n var topDoc = window.top.document;\n\n // create an iframe\n var iframeDiv = topDoc.createElement("div");\n iframeDiv.innerHTML = "<iframe scrolling=no frameborder=0 src=\'" + url + "\'>";\n var iframe = iframeDiv.firstChild;\n\n // mess with the iframe style a little\n var iframeStyle = iframe.style;\n iframeStyle.position = "absolute";\n iframeStyle.borderWidth = "0";\n iframeStyle.left = "0";\n iframeStyle.top = "0";\n iframeStyle.width = "100%";\n iframeStyle.backgroundColor = "#ffffff";\n iframeStyle.zIndex = "1";\n iframeStyle.height = "100%";\n\n // update the top window\'s document\'s body\'s style\n var hostBodyStyle = window.top.document.body.style; \n hostBodyStyle.margin = "0";\n hostBodyStyle.height = iframeStyle.height;\n hostBodyStyle.overflow = "hidden";\n\n // insert the iframe\n topDoc.body.insertBefore(iframe, topDoc.body.firstChild);\n}\n\nfunction pluginConnectionError(codeServer) {\n __gwt_displayGlassMessage(\n "Plugin failed to connect to Development Mode server at " + simpleEscape(codeServer),\n "Follow the troubleshooting instructions at "\n + "<a href=\'http://code.google.com/p/google-web-toolkit/wiki/TroubleshootingOOPHM\'>"\n + "http://code.google.com/p/google-web-toolkit/wiki/TroubleshootingOOPHM<\/a>");\n if ($errFn) {\n $errFn($moduleName);\n }\n}\n\nfunction simpleEscape(originalString) {\n return originalString.replace(/&/g,"&amp;")\n .replace(/<\/g,"&lt;")\n .replace(/>/g,"&gt;")\n .replace(/\\\'/g, "&#39;")\n .replace(/\\"/g,"&quot;");\n}\n\nfunction tryConnectingToPlugin(sessionId, url) {\n // Note that the order is important\n var pluginFinders = [findPluginXPCOM, findPluginObject, findPluginEmbed];\n var codeServer = getCodeServer();\n var plugin = null;\n for (var i = 0; i < pluginFinders.length; ++i) {\n try {\n var maybePlugin = pluginFinders[i]();\n if (maybePlugin != null && maybePlugin.init(window)) {\n plugin = maybePlugin;\n break;\n }\n } catch (e) {\n }\n }\n\n if (plugin == null) {\n // Plugin initialization failed. Show the missing-plugin page.\n return null;\n }\n if (!plugin.connect(url, sessionId, codeServer, $moduleName,\n $hostedHtmlVersion)) {\n // Connection failed. Show the error alert and troubleshooting page.\n pluginConnectionError(codeServer);\n }\n\n return plugin;\n}\n\n\n/******************************************************************************\n * Development Mode startup code\n *****************************************************************************/\nfunction gwtOnLoad(errFn, moduleName, moduleBase, softPermutationId, computePropValue) {\n $errFn = errFn;\n $moduleName = moduleName;\n $moduleBase = moduleBase;\n __gwt_getProperty = computePropValue;\n \n doBrowserSpecificFixes();\n\n if (!findPluginXPCOM()) {\n embedPlugin();\n }\n\n var topWin = window.top;\n if (!topWin.__gwt_SessionID) {\n topWin.__gwt_SessionID = generateSessionId();\n }\n\n var plugin = tryConnectingToPlugin(topWin.__gwt_SessionID, topWin.location.href);\n if (plugin == null) {\n loadIframe("http://www.gwtproject.org/missing-plugin/");\n } else {\n // take over the onunload function, wrapping any existing call if it exists\n var oldUnload = window.onunload;\n window.onunload = function() {\n // run wrapped unload first in case it is running gwt code\n !!oldUnload && oldUnload();\n try {\n // wrap in try/catch since plugins are not required to supply this\n plugin.disconnect();\n } catch (e) {\n }\n };\n }\n}\n$sendStats(\'moduleStartup\', \'moduleEvalEnd\');gwtOnLoad(__gwtModuleFunction.__errFn, __gwtModuleFunction.__moduleName, __gwtModuleFunction.__moduleBase, __gwtModuleFunction.__softPermutationId,__gwtModuleFunction.__computePropValue);$sendStats(\'moduleStartup\', \'end\');$gwt && $gwt.permProps && __gwtModuleFunction.__moduleStartupDone($gwt.permProps);\n//# sourceURL=djvu_html5-0.js\n']);
function djvu_html5(){var O='bootstrap',P='begin',Q='gwt.codesvr.djvu_html5=',R='gwt.codesvr=',S='djvu_html5',T='startup',U='DUMMY',V=0,W=1,X='iframe',Y='javascript:""',Z='position:absolute; width:0; height:0; border:none; left: -1000px;',$=' top: -1000px;',_='CSS1Compat',ab='<!doctype html>',bb='',cb='<html><head><\/head><body><\/body><\/html>',db='undefined',eb='readystatechange',fb=10,gb='Chrome',hb='eval("',ib='");',jb='script',kb='javascript',lb='moduleStartup',mb='moduleRequested',nb='Failed to load ',ob='head',pb='meta',qb='name',rb='djvu_html5::',sb='::',tb='gwt:property',ub='content',vb='=',wb='gwt:onPropertyErrorFn',xb='Bad handler "',yb='" for "gwt:onPropertyErrorFn"',zb='gwt:onLoadErrorFn',Ab='" for "gwt:onLoadErrorFn"',Bb='#',Cb='?',Db='/',Eb='img',Fb='clear.cache.gif',Gb='baseUrl',Hb='djvu_html5.nocache.js',Ib='base',Jb='//',Kb='user.agent',Lb='webkit',Mb='safari',Nb='msie',Ob=11,Pb='ie10',Qb=9,Rb='ie9',Sb=8,Tb='ie8',Ub='gecko',Vb='gecko1_8',Wb=2,Xb=3,Yb=4,Zb='selectingPermutation',$b='djvu_html5.devmode.js',_b='091ECB3AE852C68866FBC86AA8FCDB1F',ac='1A420474460884E73E3288F783AD7024',bc='3CFDA9D9AFA2798299BBAE243DB2E9B5',cc='564CB406D925C79CCD67EB98CA5AD4EF',dc='A8305F17E026239876FCBC730B035A55',ec=':',fc='.cache.js',gc='loadExternalRefs',hc='end',ic='http:',jc='file:',kc='_gwt_dummy_',lc='__gwtDevModeHook:djvu_html5',mc='Ignoring non-whitelisted Dev Mode URL: ',nc=':moduleBase';var o=window;var p=document;r(O,P);function q(){var a=o.location.search;return a.indexOf(Q)!=-1||a.indexOf(R)!=-1}
function r(a,b){if(o.__gwtStatsEvent){o.__gwtStatsEvent({moduleName:S,sessionId:o.__gwtStatsSessionId,subSystem:T,evtGroup:a,millis:(new Date).getTime(),type:b})}}
djvu_html5.__sendStats=r;djvu_html5.__moduleName=S;djvu_html5.__errFn=null;djvu_html5.__moduleBase=U;djvu_html5.__softPermutationId=V;djvu_html5.__computePropValue=null;djvu_html5.__getPropMap=null;djvu_html5.__installRunAsyncCode=function(){};djvu_html5.__gwtStartLoadingFragment=function(){return null};djvu_html5.__gwt_isKnownPropertyValue=function(){return false};djvu_html5.__gwt_getMetaProperty=function(){return null};var s=null;var t=o.__gwt_activeModules=o.__gwt_activeModules||{};t[S]={moduleName:S};djvu_html5.__moduleStartupDone=function(e){var f=t[S].bindings;t[S].bindings=function(){var a=f?f():{};var b=e[djvu_html5.__softPermutationId];for(var c=V;c<b.length;c++){var d=b[c];a[d[V]]=d[W]}return a}};var u;function v(){w();return u}
function w(){if(u){return}var a=p.createElement(X);a.src=Y;a.id=S;a.style.cssText=Z+$;a.tabIndex=-1;p.body.appendChild(a);u=a.contentDocument;if(!u){u=a.contentWindow.document}u.open();var b=document.compatMode==_?ab:bb;u.write(b+cb);u.close()}
function A(k){function l(a){function b(){if(typeof p.readyState==db){return typeof p.body!=db&&p.body!=null}return /loaded|complete/.test(p.readyState)}
var c=b();if(c){a();return}function d(){if(!c){if(!b()){return}c=true;a();if(p.removeEventListener){p.removeEventListener(eb,d,false)}if(e){clearInterval(e)}}}
if(p.addEventListener){p.addEventListener(eb,d,false)}var e=setInterval(function(){d()},fb)}
function m(c){function d(a,b){a.removeChild(b)}
var e=v();var f=e.body;var g;if(navigator.userAgent.indexOf(gb)>-1&&window.JSON){var h=e.createDocumentFragment();h.appendChild(e.createTextNode(hb));for(var i=V;i<c.length;i++){var j=window.JSON.stringify(c[i]);h.appendChild(e.createTextNode(j.substring(W,j.length-W)))}h.appendChild(e.createTextNode(ib));g=e.createElement(jb);g.language=kb;g.appendChild(h);f.appendChild(g);d(f,g)}else{for(var i=V;i<c.length;i++){g=e.createElement(jb);g.language=kb;g.text=c[i];f.appendChild(g);d(f,g)}}}
djvu_html5.onScriptDownloaded=function(a){l(function(){m(a)})};r(lb,mb);var n=p.createElement(jb);n.src=k;if(djvu_html5.__errFn){n.onerror=function(){djvu_html5.__errFn(S,new Error(nb+code))}}p.getElementsByTagName(ob)[V].appendChild(n)}
djvu_html5.__startLoadingFragment=function(a){return D(a)};djvu_html5.__installRunAsyncCode=function(a){var b=v();var c=b.body;var d=b.createElement(jb);d.language=kb;d.text=a;c.appendChild(d);c.removeChild(d)};function B(){var c={};var d;var e;var f=p.getElementsByTagName(pb);for(var g=V,h=f.length;g<h;++g){var i=f[g],j=i.getAttribute(qb),k;if(j){j=j.replace(rb,bb);if(j.indexOf(sb)>=V){continue}if(j==tb){k=i.getAttribute(ub);if(k){var l,m=k.indexOf(vb);if(m>=V){j=k.substring(V,m);l=k.substring(m+W)}else{j=k;l=bb}c[j]=l}}else if(j==wb){k=i.getAttribute(ub);if(k){try{d=eval(k)}catch(a){alert(xb+k+yb)}}}else if(j==zb){k=i.getAttribute(ub);if(k){try{e=eval(k)}catch(a){alert(xb+k+Ab)}}}}}__gwt_getMetaProperty=function(a){var b=c[a];return b==null?null:b};s=d;djvu_html5.__errFn=e}
function C(){function e(a){var b=a.lastIndexOf(Bb);if(b==-1){b=a.length}var c=a.indexOf(Cb);if(c==-1){c=a.length}var d=a.lastIndexOf(Db,Math.min(c,b));return d>=V?a.substring(V,d+W):bb}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=p.createElement(Eb);b.src=a+Fb;a=e(b.src)}return a}
function g(){var a=__gwt_getMetaProperty(Gb);if(a!=null){return a}return bb}
function h(){var a=p.getElementsByTagName(jb);for(var b=V;b<a.length;++b){if(a[b].src.indexOf(Hb)!=-1){return e(a[b].src)}}return bb}
function i(){var a=p.getElementsByTagName(Ib);if(a.length>V){return a[a.length-W].href}return bb}
function j(){var a=p.location;return a.href==a.protocol+Jb+a.host+a.pathname+a.search+a.hash}
var k=g();if(k==bb){k=h()}if(k==bb){k=i()}if(k==bb&&j()){k=e(p.location.href)}k=f(k);return k}
function D(a){if(a.match(/^\//)){return a}if(a.match(/^[a-zA-Z]+:\/\//)){return a}return djvu_html5.__moduleBase+a}
function F(){var f=[];var g=V;function h(a,b){var c=f;for(var d=V,e=a.length-W;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
var i=[];var j=[];function k(a){var b=j[a](),c=i[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(s){s(a,d,b)}throw null}
j[Kb]=function(){var a=navigator.userAgent.toLowerCase();var b=p.documentMode;if(function(){return a.indexOf(Lb)!=-1}())return Mb;if(function(){return a.indexOf(Nb)!=-1&&(b>=fb&&b<Ob)}())return Pb;if(function(){return a.indexOf(Nb)!=-1&&(b>=Qb&&b<Ob)}())return Rb;if(function(){return a.indexOf(Nb)!=-1&&(b>=Sb&&b<Ob)}())return Tb;if(function(){return a.indexOf(Ub)!=-1||b>=Ob}())return Vb;return bb};i[Kb]={'gecko1_8':V,'ie10':W,'ie8':Wb,'ie9':Xb,'safari':Yb};__gwt_isKnownPropertyValue=function(a,b){return b in i[a]};djvu_html5.__getPropMap=function(){var a={};for(var b in i){if(i.hasOwnProperty(b)){a[b]=k(b)}}return a};djvu_html5.__computePropValue=k;o.__gwt_activeModules[S].bindings=djvu_html5.__getPropMap;r(O,Zb);if(q()){return D($b)}var l;try{h([Tb],_b);h([Pb],ac);h([Mb],bc);h([Vb],cc);h([Rb],dc);l=f[k(Kb)];var m=l.indexOf(ec);if(m!=-1){g=parseInt(l.substring(m+W),fb);l=l.substring(V,m)}}catch(a){}djvu_html5.__softPermutationId=g;return D(l+fc)}
function G(){if(!o.__gwt_stylesLoaded){o.__gwt_stylesLoaded={}}r(gc,P);r(gc,hc)}
B();djvu_html5.__moduleBase=C();t[S].moduleBase=djvu_html5.__moduleBase;var H=F();if(o){var I=!!(o.location.protocol==ic||o.location.protocol==jc);o.__gwt_activeModules[S].canRedirect=I;function J(){var b=kc;try{o.sessionStorage.setItem(b,b);o.sessionStorage.removeItem(b);return true}catch(a){return false}}
if(I&&J()){var K=lc;var L=o.sessionStorage[K];if(!/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/.*$/.test(L)){if(L&&(window.console&&console.log)){console.log(mc+L)}L=bb}if(L&&!o[K]){o[K]=true;o[K+nc]=C();var M=p.createElement(jb);M.src=L;var N=p.getElementsByTagName(ob)[V];N.insertBefore(M,N.firstElementChild||N.children[V]);return false}}}G();r(O,hc);A(H);return true}
djvu_html5.succeeded=djvu_html5();
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
self.$wnd=self;self.$doc=self;function djvu_worker(){var c='81EB501BD7AB47786C30D3175CE1EA2B',d='.cache.js',e='djvu_worker',f='';var b;try{b=c}catch(a){return}importScripts(b+d);gwtOnLoad(undefined,e,f)}
djvu_worker();
\ No newline at end of file
......@@ -87,7 +87,7 @@ $(".container-fluid").bind('drop', function (e) {
var files = e.originalEvent.dataTransfer.files;
var test = $("#btn-upload")[0].accept;
$(this).css('background', '');
const dt = new DataTransfer()
const dt = new DataTransfer();
jQuery.each(files, function (index, item) {
if (test.indexOf(item.name.substr(item.name.lastIndexOf('.'))) !== -1) {
dt.items.add(item);
......
......@@ -41,7 +41,7 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro
else:
exc_command = [x for x in command]
return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env)
return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env) # nosec
def process_wait(command, serr=subprocess.PIPE):
......
......@@ -55,6 +55,7 @@
{% endif %}
{% if reader_list and g.user.role_viewer() %}
<div class="btn-group" role="group">
{% if reader_list|length > 1 %}
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in Browser')}}
<span class="caret"></span>
......@@ -64,15 +65,19 @@
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
</ul>
{% else %}
<a target="_blank" href="{{url_for('web.read_book', book_id=entry.id, book_format=reader_list[0])}}" id="readbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-eye-open"></span> {{_('Read in Browser')}} - {{reader_list[0]}}</a>
{% endif %}
</div>
{% endif %}
{% if audioentries|length > 0 and g.user.role_viewer() %}
<div class="btn-group" role="group">
{% if audioentries|length > 1 %}
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-music"></span> {{_('Listen in Browser')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
{% for format in reader_list %}
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
......@@ -85,6 +90,9 @@
{% endif %}
{% endfor %}
</ul>
{% else %}
<a target="_blank" href="{{url_for('web.read_book', book_id=entry.id, book_format=audioentries[0])}}" id="listenbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-music"></span> {{_('Listen in Browser')}} - {{audioentries[0]}}</a>
{% endif %}
</div>
{% endif %}
</div>
......
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/libs/djvu_html5/Djvu_html5.css') }}">
<title>Djvu HTML5 browser demo</title>
<script type="text/javascript" language="javascript"
src="{{ url_for('static', filename='js/libs/djvu_html5/djvu_html5/djvu_html5.nocache.js') }}"></script>
</head>
<body>
<!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
<noscript>
<div
style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
Your web browser must have JavaScript enabled in order for this
application to display correctly.</div>
</noscript>
<div id="djvuContainer" file="{{ url_for('web.serve_book', book_id=djvufile,book_format='djvu') }}"></div>
<script type="text/javascript">
var DJVU_CONTEXT = {
background: "#666",
uiHideDelay: 1500,
};
</script>
</body>
</html>
......@@ -289,6 +289,64 @@ class Updater(threading.Thread):
def _stable_version_info(cls):
return constants.STABLE_VERSION # Current version
@staticmethod
def _populate_parent_commits(update_data, status, locale, tz, parents):
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except (IndexError, KeyError):
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(parent_commit['url'], headers=headers, timeout=10)
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=locale)
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n', '<p>').replace('\n', '<p>')])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
return parents
@staticmethod
def _load_nightly_data(repository_url, commit, status):
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'],
headers=headers,
timeout=10)
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['message'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except (requests.exceptions.RequestException, ValueError):
status['message'] = _(u'General error')
return status, update_data
def _nightly_available_updates(self, request_method, locale):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if request_method == "GET":
......@@ -314,22 +372,7 @@ class Updater(threading.Thread):
# a new update is available
status['update'] = True
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'],
headers=headers,
timeout=10)
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['message'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except (requests.exceptions.RequestException, ValueError):
status['message'] = _(u'General error')
status, update_data = self._load_nightly_data(repository_url, commit, status)
if status['message'] != '':
return json.dumps(status)
......@@ -351,41 +394,7 @@ class Updater(threading.Thread):
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except (IndexError, KeyError):
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(parent_commit['url'], headers=headers, timeout=10)
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=locale)
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n', '<p>').replace('\n', '<p>')])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
parents = self._populate_parent_commits(update_data, status, locale, tz, parents)
status['history'] = parents[::-1]
except (IndexError, KeyError):
status['success'] = False
......
......@@ -191,7 +191,7 @@ def upload(uploadfile, rarExcecutable):
filename = uploadfile.filename
filename_root, file_extension = os.path.splitext(filename)
md5 = hashlib.md5(filename.encode('utf-8')).hexdigest()
md5 = hashlib.md5(filename.encode('utf-8')).hexdigest() # nosec
tmp_file_path = os.path.join(tmp_dir, md5)
log.debug("Temporary file: %s", tmp_file_path)
uploadfile.save(tmp_file_path)
......
......@@ -1609,6 +1609,9 @@ def read_book(book_id, book_format):
elif book_format.lower() == "txt":
log.debug(u"Start txt reader for %d", book_id)
return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book"))
elif book_format.lower() == "djvu":
log.debug(u"Start djvu reader for %d", book_id)
return render_title_template('readdjvu.html', djvufile=book_id, title=_(u"Read a Book"))
else:
for fileExt in constants.EXTENSIONS_AUDIO:
if book_format.lower() == fileExt:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment