Merge branch 'master' into Develop

# Conflicts:
#	cps/opds.py
#	cps/server.py
#	cps/web.py
This commit is contained in:
Ozzie Isaacs 2023-02-05 12:10:01 +01:00
commit 4b7a0f3662
87 changed files with 5948 additions and 5978 deletions

1
.gitattributes vendored
View File

@ -1,4 +1,5 @@
constants.py ident export-subst
/test export-ignore
/library export-ignore
cps/static/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ cps/cache
.idea/
*.bak
*.log.*
.key
settings.yaml
gdrive_credentials

1
.key
View File

@ -1 +0,0 @@
onLmA_LND5S8jNSvi8nNSGwevE13f7t8pW-wgWAXZgo=

View File

@ -52,7 +52,8 @@ In the Wiki there are also examples for: a [manual installation](https://github.
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \
Login with default admin login \
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button \
If you don't have a Calibre database already, this [database](https://github.com/janeczku/calibre-web/blob/master/library/metadata.db) can be used. **IMPORTATNT** Please move the database out of the calibre-web folder structure, as it will be overwritten during update. \
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button. \
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration) \
Afterwards you can configure your Calibre-Web instance ([Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) on admin page)

View File

@ -21,9 +21,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask_login import LoginManager
from flask import session
from flask_login import LoginManager, confirm_login
from flask import session, current_app
from flask_login.utils import decode_cookie
from flask_login.signals import user_loaded_from_cookie
class MyLoginManager(LoginManager):
def _session_protection_failed(self):
@ -33,3 +34,19 @@ class MyLoginManager(LoginManager):
and _session.get('csrf_token', None))) and ident != _session.get('_id', None):
return super(). _session_protection_failed()
return False
def _load_user_from_remember_cookie(self, cookie):
user_id = decode_cookie(cookie)
if user_id is not None:
session["_user_id"] = user_id
session["_fresh"] = False
user = None
if self._user_callback:
user = self._user_callback(user_id)
if user is not None:
app = current_app._get_current_object()
user_loaded_from_cookie.send(app, user=user)
# if session was restored from remember me cookie make login valid
confirm_login()
return user
return None

View File

@ -33,7 +33,7 @@ from datetime import time as datetime_time
from functools import wraps
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask_login import login_required, current_user, logout_user, confirm_login
from flask_login import login_required, current_user, logout_user
from flask_babel import gettext as _
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
from flask import session as flask_session
@ -101,21 +101,16 @@ def admin_required(f):
@admi.before_app_request
def before_request():
# make remember me function work
if current_user.is_authenticated:
confirm_login()
if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path:
logout_user()
g.constants = constants
g.user = current_user
# g.user = current_user
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
g.allow_registration = config.config_public_reg
g.allow_anonymous = config.config_anonbrowse
g.allow_upload = config.config_uploading
g.current_theme = config.config_theme
g.config_authors_max = config.config_authors_max
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
if '/static/' not in request.path and not config.db_configured and \
request.endpoint not in ('admin.ajax_db_config',
'admin.simulatedbchange',

View File

@ -1,7 +1,8 @@
from babel import negotiate_locale
from flask_babel import Babel, Locale
from babel.core import UnknownLocaleError
from flask import request, g
from flask import request
from flask_login import current_user
from . import logger
@ -11,10 +12,10 @@ babel = Babel()
def get_locale():
# if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None)
if user is not None and hasattr(user, "locale"):
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
if current_user is not None and hasattr(current_user, "locale"):
# if the account is the guest account bypass the config lang settings
if current_user.name != 'Guest':
return current_user.locale
preferred = list()
if request.accept_languages:

View File

@ -163,7 +163,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher, pubdate, identifiers')
STABLE_VERSION = {'version': '0.6.19'}
STABLE_VERSION = {'version': '0.6.20 Beta'}
NIGHTLY_VERSION = dict()
NIGHTLY_VERSION[0] = '$Format:%H$'

View File

@ -22,41 +22,26 @@
import datetime
from urllib.parse import unquote_plus
from functools import wraps
from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask import Blueprint, request, render_template, make_response, abort
from flask_login import current_user
from flask_babel import get_locale
from flask_babel import gettext as _
from sqlalchemy.sql.expression import func, text, or_, and_, true
from sqlalchemy.exc import InvalidRequestError, OperationalError
from werkzeug.security import check_password_hash
from . import constants, logger, config, db, calibre_db, ub, services, isoLanguages, limiter
from . import logger, config, db, calibre_db, ub, isoLanguages
from .usermanagement import requires_basic_auth_if_no_ano
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
from .web import render_read_books
from .usermanagement import load_user_from_request
from flask_babel import gettext as _
from flask_limiter import RateLimitExceeded
opds = Blueprint('opds', __name__)
log = logger.create()
def requires_basic_auth_if_no_ano(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if config.config_anonbrowse != 1:
if not auth or auth.type != 'basic' or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and config.config_anonbrowse != 1:
return services.ldap.basic_auth_required(f)
return decorated
@opds.route("/opds/")
@opds.route("/opds")
@requires_basic_auth_if_no_ano
@ -356,7 +341,8 @@ def feed_languages(book_id):
@requires_basic_auth_if_no_ano
def feed_shelfindex():
off = request.args.get("offset") or 0
shelf = g.shelves_access
shelf = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
number = len(shelf)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
number)
@ -403,11 +389,7 @@ def feed_shelf(book_id):
@opds.route("/opds/download/<book_id>/<book_format>/")
@requires_basic_auth_if_no_ano
def opds_download_link(book_id, book_format):
# I gave up with this: With enabled ldap login, the user doesn't get logged in, therefore it's always guest
# workaround, loading the user from the request and checking its download rights here
# in case of anonymous browsing user is None
user = load_user_from_request(request) or current_user
if not user.role_download():
if not current_user.role_download():
return abort(403)
if "Kobo" in request.headers.get('User-Agent'):
client = "kobo"
@ -479,32 +461,6 @@ def feed_search(term):
return render_xml_template('feed.xml', searchterm="")
def check_auth(username, password):
try:
limiter.check()
except RateLimitExceeded:
return abort(429) # False
try:
username = username.encode('windows-1252')
except UnicodeEncodeError:
username = username.encode('utf-8')
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
username.decode('utf-8').lower()).first()
if bool(user and check_password_hash(str(user.password), password)):
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
return True
else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_address)
return False
def authenticate():
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def render_xml_template(*args, **kwargs):
# ToDo: return time in current timezone similar to %z

View File

@ -20,11 +20,13 @@ from flask import render_template, g, abort, request
from flask_babel import gettext as _
from werkzeug.local import LocalProxy
from flask_login import current_user
from sqlalchemy.sql.expression import or_
from . import config, constants, logger
from . import config, constants, logger, ub
from .ub import User
log = logger.create()
def get_sidebar_config(kwargs=None):
@ -45,12 +47,12 @@ def get_sidebar_config(kwargs=None):
"show_text": _('Show Hot Books'), "config_show": True})
if current_user.role_admin():
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
else:
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
sidebar.append(
@ -58,11 +60,11 @@ def get_sidebar_config(kwargs=None):
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
"show_text": _('Show Top Rated Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous),
"page": "read", "show_text": _('Show Read and Unread'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous), "page": "unread",
"show_text": _('Show unread'), "config_show": False})
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
@ -81,7 +83,7 @@ def get_sidebar_config(kwargs=None):
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
"show_text": _('Show Publisher Section'), "config_show":True})
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
"page": "language",
"show_text": _('Show Language Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
@ -92,13 +94,16 @@ def get_sidebar_config(kwargs=None):
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived",
"show_text": _('Show Archived Books'), "config_show": content})
if not simple:
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
return sidebar, simple

View File

@ -35,12 +35,13 @@ search = Blueprint('search', __name__)
log = logger.create()
@search.route("/search", methods=["GET"])
@search.route("/search", methods=["POST"])
@login_required_if_no_ano
def simple_search():
term = request.args.get("query")
term = dict(request.form).get("query")
if term:
return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
flask_session['query'] = json.dumps(term.strip())
return redirect(url_for('web.books_list', data="search", sort_param='stored', query="")) # term.strip()
else:
return render_title_template('search.html',
searchterm="",

View File

@ -269,7 +269,7 @@ class WebServer(object):
@staticmethod
def shutdown_scheduler():
scheduler = BackgroundScheduler()
if scheduler: # and not scheduler.scheduler.STATE_STOPPED:
if scheduler:
scheduler.scheduler.shutdown()
def _killServer(self, __, ___):

View File

@ -140,6 +140,7 @@ table .bg-dark-danger a { color: #fff; }
.container-fluid .book {
margin-top: 20px;
max-width: 180px;
display: flex;
flex-direction: column;
}

File diff suppressed because one or more lines are too long

View File

@ -364,12 +364,6 @@ $(function() {
layoutMode : "fitRows"
});
$(".grid").isotope({
// options
itemSelector : ".grid-item",
layoutMode : "fitColumns"
});
if ($(".load-more").length && $(".next").length) {
var $loadMore = $(".load-more .row").infiniteScroll({
debug: false,

View File

@ -6,7 +6,7 @@
<!-- Always use full-sized image for the book edit page -->
<img id="detailcover" title="{{book.title}}" src="{{url_for('web.get_cover', book_id=book.id, resolution='og', c=book|last_modified)}}" />
</div>
{% if g.user.role_delete_books() %}
{% if current_user.role_delete_books() %}
<div class="text-center">
<button type="button" class="btn btn-danger" id="delete" data-toggle="modal" data-delete-id="{{ book.id }}" data-target="#deleteModal">{{_("Delete Book")}}</button>
</div>
@ -99,7 +99,7 @@
<label for="rating">{{_('Rating')}}</label>
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
</div>
{% if g.user.role_upload() and g.allow_upload %}
{% if current_user.role_upload() and g.allow_upload %}
<div class="form-group">
<label for="cover_url">{{_('Fetch Cover from URL (JPEG - Image will be downloaded and stored in database)')}}</label>
<input type="text" class="form-control" name="cover_url" id="cover_url" value="">
@ -196,7 +196,7 @@
</div>
{% endfor %}
{% endif %}
{% if g.user.role_upload() and g.allow_upload %}
{% if current_user.role_upload() and g.allow_upload %}
<div role="group" aria-label="Upload new book format">
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
<div class="upload-format-input-text" id="upload-format"></div>
@ -219,7 +219,7 @@
{% endblock %}
{% block modal %}
{{ delete_book() }}
{{ delete_book(current_user.role_delete_books()) }}
{{ delete_confirm_modal() }}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
@ -291,7 +291,7 @@
'description': {{_('Description')|safe|tojson}},
'source': {{_('Source')|safe|tojson}},
};
var language = '{{ g.user.locale }}';
var language = '{{ current_user.locale }}';
$("#add-identifier-line").click(function() {
// create a random identifier type to have a valid name in form. This will not be used when dealing with the form
@ -313,8 +313,8 @@
<script src="{{ url_for('static', filename='js/get_meta.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/tinymce/tinymce.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/bootstrap-datepicker.min.js') }}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% if not current_user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.' + current_user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
<script src="{{ url_for('static', filename='js/fullscreen.js') }}"></script>

View File

@ -4,7 +4,7 @@
{% if sort %}data-sortable="true" {% endif %}
data-visible = "{{visiblility.get(parameter)}}"
data-escape="true"
{% if g.user.role_edit() %}
{% if current_user.role_edit() %}
data-editable-type="text"
data-editable-url="{{ url_for('edit-book.edit_list_book', param=parameter)}}"
data-editable-title="{{ edit_text }}"
@ -53,10 +53,10 @@
</div>
<table id="books-table" class="table table-no-bordered table-striped"
data-url="{{url_for('web.list_books')}}" data-locale="{{ g.user.locale }}">
data-url="{{url_for('web.list_books')}}" data-locale="{{ current_user.locale }}">
<thead>
<tr>
{% if g.user.role_edit() %}
{% if current_user.role_edit() %}
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
{% endif %}
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
@ -66,37 +66,37 @@
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false, true) }}
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('edit-book.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter Title')}}"{% endif %}>{{_('Series Index')}}</th>
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if current_user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('edit-book.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter Title')}}"{% endif %}>{{_('Series Index')}}</th>
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, true) }}
<!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, true) }}
<th data-field="comments" id="comments" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('comments')}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='comments')}}" data-edit="true" data-editable-title="{{_('Enter comments')}}"{% endif %}>{{_('Comments')}}</th>
{% if g.user.check_visibility(32768) %}
<th data-field="comments" id="comments" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('comments')}}" data-sortable="false" {% if current_user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='comments')}}" data-edit="true" data-editable-title="{{_('Enter comments')}}"{% endif %}>{{_('Comments')}}</th>
{% if current_user.check_visibility(32768) %}
{{ book_checkbox_row('is_archived', _('Archive Status'), false)}}
{% endif %}
{{ book_checkbox_row('read_status', _('Read Status'), false)}}
{% for c in cc %}
{% if c.datatype == "int" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="1" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if current_user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="1" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "rating" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-formatter="ratingFormatter" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.5" data-editable-step="1" data-editable-min="1" data-editable-max="5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-formatter="ratingFormatter" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if current_user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.5" data-editable-step="1" data-editable-min="1" data-editable-max="5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "float" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if current_user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "enumeration" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="select" data-editable-source={{ url_for('edit-book.table_get_custom_enum', c_id=c.id) }} data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if current_user.role_edit() %} data-editable-type="select" data-editable-source={{ url_for('edit-book.table_get_custom_enum', c_id=c.id) }} data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype in ["datetime"] %}
<!-- missing -->
{% elif c.datatype == "text" %}
{{ text_table_row('custom_column_' + c.id|string, _('Enter ') + c.name, c.name, false, false) }}
{% elif c.datatype == "comments" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if current_user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "bool" %}
{{ book_checkbox_row('custom_column_' + c.id|string, c.name, false)}}
{% else %}
<!--{{ text_table_row('custom_column_' + c.id|string, _('Enter ') + c.name, c.name, false, false) }} -->
{% endif %}
{% endfor %}
{% if g.user.role_delete_books() and g.user.role_edit()%}
{% if current_user.role_delete_books() and current_user.role_edit()%}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %}
</tr>
@ -104,8 +104,8 @@
</table>
{% endblock %}
{% block modal %}
{{ delete_book() }}
{% if g.user.role_edit() %}
{{ delete_book(current_user.role_delete_books()) }}
{% if current_user.role_edit() %}
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
<div class="modal-dialog">
<div class="modal-content">
@ -137,8 +137,8 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-locale-all.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/locale/bootstrap-table-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% if not current_user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/locale/bootstrap-table-' + current_user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/libs/wysihtml5-0.3.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-wysihtml5-0.0.3.min.js') }}"></script>

View File

@ -11,7 +11,7 @@
<div class="col-sm-9 col-lg-9 book-meta">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Download, send to eReader, reading">
{% if g.user.role_download() %}
{% if current_user.role_download() %}
{% if entry.data|length %}
<div class="btn-group" role="group">
{% if entry.data|length < 2 %}
@ -37,7 +37,7 @@
</div>
{% endif %}
{% endif %}
{% if g.user.kindle_mail and entry.email_share_list %}
{% if current_user.kindle_mail and entry.email_share_list %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if entry.email_share_list.__len__() == 1 %}
<div id="sendbtn" data-action="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=entry.email_share_list[0]['format'], convert=entry.email_share_list[0]['convert'])}}" data-text="{{_('Send to eReader')}}" class="btn btn-primary postAction" role="button"><span class="glyphicon glyphicon-send"></span> {{entry.email_share_list[0]['text']}}</div>
@ -55,7 +55,7 @@
</div>
{% endif %}
{% endif %}
{% if entry.reader_list and g.user.role_viewer() %}
{% if entry.reader_list and current_user.role_viewer() %}
<div class="btn-group" role="group">
{% if entry.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">
@ -72,7 +72,7 @@
{% endif %}
</div>
{% endif %}
{% if entry.audio_entries|length > 0 and g.user.role_viewer() %}
{% if entry.audio_entries|length > 0 and current_user.role_viewer() %}
<div class="btn-group" role="group">
{% if entry.audio_entries|length > 1 %}
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -213,7 +213,7 @@
</div>
{% endfor %}
{% endif %}
{% if not g.user.is_anonymous %}
{% if not current_user.is_anonymous %}
<div class="custom_columns">
<p>
@ -225,7 +225,7 @@
</label>
</form>
</p>
{% if g.user.check_visibility(32768) %}
{% if current_user.check_visibility(32768) %}
<p>
<form id="archived_form" action="{{ url_for('web.toggle_archived', book_id=entry.id)}}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@ -250,8 +250,8 @@
<div class="more-stuff">
{% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.shelves_access %}
{% if current_user.is_authenticated %}
{% if current_user.shelf.all() or g.shelves_access %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Add to shelves">
<button id="add-to-shelf" type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -260,7 +260,7 @@
</button>
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
{% for shelf in g.shelves_access %}
{% if not shelf.id in books_shelfs and ( not shelf.is_public or g.user.role_edit_shelfs() ) %}
{% if not shelf.id in books_shelfs and ( not shelf.is_public or current_user.role_edit_shelfs() ) %}
<li>
<a data-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
data-remove-href="{{ url_for('shelf.remove_from_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
@ -281,7 +281,7 @@
data-add-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
class="btn btn-sm btn-default" role="button" data-shelf-action="remove"
>
<span {% if not shelf.is_public or g.user.role_edit_shelfs() %}
<span {% if not shelf.is_public or current_user.role_edit_shelfs() %}
class="glyphicon glyphicon-remove"
{% endif %}></span> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}
</a>
@ -294,7 +294,7 @@
{% endif %}
{% endif %}
{% if g.user.role_edit() %}
{% if current_user.role_edit() %}
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book">
<a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>

View File

@ -80,7 +80,7 @@
</div>
<button id="domain_allow_submit" class="btn btn-default">{{_('Add')}}</button>
</form>
<table class="table table-no-bordered" id="domain-allow-table" data-url="{{url_for('admin.list_domain', allow=1)}}" data-id-field="id" data-show-header="false" data-editable-mode="inline" data-locale="{{ g.user.locale }}">
<table class="table table-no-bordered" id="domain-allow-table" data-url="{{url_for('admin.list_domain', allow=1)}}" data-id-field="id" data-show-header="false" data-editable-mode="inline" data-locale="{{ current_user.locale }}">
<thead>
<tr>
<th data-field="domain" id="domain-allow" data-escape="true" data-editable-type="text" data-editable-url="{{ url_for('admin.edit_domain', allow = 1)}}" data-editable="true" data-editable-title="{{_('Enter domainname')}}"></th>
@ -90,7 +90,7 @@
</thead>
</table>
<h2>{{_('Denied Domains (Blacklist)')}}</h2>
<table class="table table-no-bordered" id="domain-deny-table" data-url="{{url_for('admin.list_domain', allow=0)}}" data-id-field="id" data-show-header="false" data-editable-mode="inline" data-locale="{{ g.user.locale }}">
<table class="table table-no-bordered" id="domain-deny-table" data-url="{{url_for('admin.list_domain', allow=0)}}" data-id-field="id" data-show-header="false" data-editable-mode="inline" data-locale="{{ current_user.locale }}">
<thead>
<tr>
<th data-field="domain" id="domain-deny" data-escape="true" data-editable-type="text" data-editable-url="{{ url_for('admin.edit_domain', allow = 0)}}" data-editable="true" data-editable-title="{{_('Enter domainname')}}"></th>

View File

@ -1,7 +1,7 @@
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
{% if g.user.show_detail_random() and page != "discover" %}
{% if current_user.show_detail_random() and page != "discover" %}
<div class="discover random-books">
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
<div class="row display-flex">

View File

@ -1,7 +1,7 @@
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal, change_confirm_modal %}
{% import 'image.html' as image %}
<!DOCTYPE html>
<html lang="{{ g.user.locale }}">
<html lang="{{ current_user.locale }}">
<head>
<title>{{instance}} | {{title}}</title>
<meta charset="utf-8">
@ -40,8 +40,9 @@
<div class="home-btn"><a class="home-btn-tooltip" href="/" data-toggle="tooltip" title="" data-placement="bottom" data-original-title="Home"></a></div>
<div class="plexBack"><a href="{{url_for('web.index')}}"></a></div>
{% endif %}
{% if g.user.is_authenticated or g.allow_anonymous %}
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="GET">
{% if current_user.is_authenticated or g.allow_anonymous %}
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group input-group input-group-sm">
<label for="query" class="sr-only">{{_('Search')}}</label>
<input type="text" class="form-control" id="query" name="query" placeholder="{{_('Search Library')}}" value="{{searchterm}}">
@ -52,28 +53,28 @@
</form>
{% endif %}
<div class="navbar-collapse collapse">
{% if g.user.is_authenticated or g.allow_anonymous %}
{% if current_user.is_authenticated or g.allow_anonymous %}
<ul class="nav navbar-nav ">
<li><a href="{{url_for('search.advanced_search')}}" id="advanced_search"><span class="glyphicon glyphicon-search"></span><span class="hidden-sm"> {{_('Advanced Search')}}</span></a></li>
</ul>
{% endif %}
<ul class="nav navbar-nav navbar-right" id="main-nav">
{% if g.user.is_authenticated or g.allow_anonymous %}
{% if current_user.is_authenticated or g.allow_anonymous %}
{% if g.current_theme == 1 %}
<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a>
<ul class="dropdown-menu profileDropli">
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{g.user.name}}</span></a></li>
{% if g.allow_registration and not g.user.is_authenticated %}
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{current_user.name}}</span></a></li>
{% if g.allow_registration and not current_user.is_authenticated %}
<li><a id="login" href="{{url_for('web.login')}}"><span class="glyphicon glyphicon-log-in"></span> {{_('Login')}}</a></li>
<li><a id="register" href="{{url_for('web.register')}}"><span class="glyphicon glyphicon-user"></span> {{_('Register')}}</a></li>
{% endif %}
{% if not g.user.is_anonymous %}
{% if not current_user.is_anonymous %}
<li><a id="logout" href="{{url_for('web.logout')}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
{% endif %}
</ul>
</li>
{% endif %}
{% if g.user.role_upload() and g.allow_upload %}
{% if current_user.role_upload() and g.allow_upload %}
<li>
<form id="form-upload" class="navbar-form" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@ -84,20 +85,20 @@
</form>
</li>
{% endif %}
{% if not g.user.is_anonymous and not simple%}
{% if not current_user.is_anonymous and not simple%}
<li class="top_tasks"><a id="top_tasks" href="{{url_for('tasks.get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span> <span class="hidden-sm">{{_('Tasks')}}</span></a></li>
{% endif %}
{% if g.user.role_admin() %}
{% if current_user.role_admin() %}
<li><a id="top_admin" data-text="{{_('Settings')}}" href="{{url_for('admin.admin')}}"><span class="glyphicon glyphicon-dashboard"></span> <span class="hidden-sm">{{_('Admin')}}</span></a></li>
{% endif %}
{% if g.current_theme == 0 %}
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{g.user.name}}</span></a></li>
{% if not g.user.is_anonymous %}
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{current_user.name}}</span></a></li>
{% if not current_user.is_anonymous %}
<li><a id="logout" href="{{url_for('web.logout')}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
{% endif %}
{% endif %}
{% endif %}
{% if g.allow_registration and not g.user.is_authenticated and g.current_theme == 0 %}
{% if g.allow_registration and not current_user.is_authenticated and g.current_theme == 0 %}
<li><a id="login" href="{{url_for('web.login')}}"><span class="glyphicon glyphicon-log-in"></span> {{_('Login')}}</a></li>
<li><a id="register" href="{{url_for('web.register')}}"><span class="glyphicon glyphicon-user"></span> {{_('Register')}}</a></li>
{% endif %}
@ -138,22 +139,22 @@
{%endif%}
<div class="container-fluid">
<div class="row-fluid">
{% if g.user.is_authenticated or g.allow_anonymous %}
{% if current_user.is_authenticated or g.allow_anonymous %}
<div class="col-sm-2">
<nav class="navigation">
<ul class="list-unstyled" id="scnd-nav" intent in-standard-append="nav.navigation" in-mobile-after="#main-nav" in-mobile-class="nav navbar-nav">
<li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %}
{% if g.user.check_visibility(element['visibility']) and element['public'] %}
{% if current_user.check_visibility(element['visibility']) and element['public'] %}
<li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort_param='stored')}}"><span class="glyphicon {{element['glyph']}}"></span> {{_(element['text'])}}</a></li>
{% endif %}
{% endfor %}
{% if g.user.is_authenticated or g.allow_anonymous %}
{% if current_user.is_authenticated or g.allow_anonymous %}
<li class="nav-head hidden-xs public-shelves">{{_('Shelves')}}</li>
{% for shelf in g.shelves_access %}
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span> {{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endfor %}
{% if not g.user.is_anonymous %}
{% if not current_user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li>
{% endif %}

View File

@ -149,7 +149,7 @@
</div>
{% endfor %}
{% endif %}
{% if not g.user.is_anonymous %}
{% if not current_user.is_anonymous %}
<div class="custom_columns">
<p>
@ -159,7 +159,7 @@
<span>{{_('Read')}}</span>
</label>
</p>
{% if g.user.check_visibility(32768) %}
{% if current_user.check_visibility(32768) %}
<p>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label class="block-label">
@ -182,8 +182,8 @@
<div class="more-stuff">
{% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.shelves_access %}
{% if current_user.is_authenticated %}
{% if current_user.shelf.all() or g.shelves_access %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Add to shelves">
<button id="add-to-shelf" type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -192,7 +192,7 @@
</button>
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
{% for shelf in g.shelves_access %}
{% if not shelf.id in books_shelfs and ( not shelf.is_public or g.user.role_edit_shelfs() ) %}
{% if not shelf.id in books_shelfs and ( not shelf.is_public or current_user.role_edit_shelfs() ) %}
<li>
<a data-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
data-remove-href="{{ url_for('shelf.remove_from_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
@ -213,7 +213,7 @@
data-add-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
class="btn btn-sm btn-default" role="button" data-shelf-action="remove"
>
<span {% if not shelf.is_public or g.user.role_edit_shelfs() %}
<span {% if not shelf.is_public or current_user.role_edit_shelfs() %}
class="glyphicon glyphicon-remove"
{% endif %}></span> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}
</a>
@ -343,7 +343,7 @@ window.calibre = {
bookUrl: "{{ url_for('static', filename=mp3file) }}/",
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=mp3file, book_format=audioformat.upper()) }}",
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
useBookmarks: "{{ g.user.is_authenticated | tojson }}"
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
};
</script>

View File

@ -37,8 +37,8 @@
</div>
</div>
{% endmacro %}
{% macro delete_book() %}
{% if g.user.role_delete_books() %}
{% macro delete_book(allow) %}
{% if allow %}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog">
<div class="modal-content">

View File

@ -108,7 +108,7 @@
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=bookid, book_format='EPUB') }}",
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format='epub', anyname='file.epub') }}",
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
useBookmarks: "{{ g.user.is_authenticated | tojson }}"
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
};
function selectTheme(id) {

View File

@ -171,7 +171,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
</button>
{% if g.user.role_download() %}
{% if current_user.role_download() %}
<button id="secondaryPrint" class="secondaryToolbarButton visibleMediumView" title="Print" tabindex="53" data-l10n-id="print">
<span data-l10n-id="print_label">Print</span>
</button>
@ -280,7 +280,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
</button>
{% if g.user.role_download() %}
{% if current_user.role_download() %}
<button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print">
<span data-l10n-id="print_label">Print</span>
</button>

View File

@ -7,8 +7,8 @@
<p>{{_('Search Term:')}} {{adv_searchterm}}</p>
{% else %}
<h2>{{result_count}} {{_('Results for:')}} {{adv_searchterm}}</h2>
{% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.shelves_access %}
{% if current_user.is_authenticated %}
{% if current_user.shelf.all() or g.shelves_access %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="btn-group" role="group" aria-label="Add to shelves">
@ -18,7 +18,7 @@
</button>
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
{% for shelf in g.shelves_access %}
{% if not shelf.is_public or g.user.role_edit_shelfs() %}
{% if not shelf.is_public or current_user.role_edit_shelfs() %}
<li><a class="postAction" role="button" data-action="{{ url_for('shelf.search_to_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endif %}
{%endfor%}

View File

@ -230,18 +230,18 @@
{% block js %}
<script>
var language = '{{ g.user.locale }}';
var language = '{{ current_user.locale }}';
</script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/bootstrap-datepicker.min.js') }}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% if not current_user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.' + current_user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-select.min.js')}}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% if not current_user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + current_user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
{% endblock %}
{% block header %}

View File

@ -4,11 +4,11 @@
<div class="discover">
<h2>{{title}}</h2>
<!--form method="post"--->
{% if g.user.role_download() %}
{% if current_user.role_download() %}
<a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% endif %}
{% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
{% if current_user.is_authenticated %}
{% if (current_user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="btn btn-danger" data-action="{{url_for('shelf.delete_shelf', shelf_id=shelf.id)}}" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>

View File

@ -8,7 +8,7 @@
<label for="title">{{_('Title')}}</label>
<input type="text" class="form-control" name="title" id="title" value="{{ shelf.name if shelf.name != None }}">
</div>
{% if g.user.role_edit_shelfs() %}
{% if current_user.role_edit_shelfs() %}
<div class="checkbox">
<label>
<input type="checkbox" name="is_public" {% if shelf.is_public == 1 %}checked{% endif %}> {{_('Share with Everyone')}}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ g.user.locale }}">
<html lang="{{ current_user.locale }}">
<head>
<title>{{instance}} | {{title}}</title>
<meta charset="utf-8">
@ -58,7 +58,7 @@
</div>
<div class="btn-group" role="group" aria-label="Download, send to eReader, reading">
{% if g.user.role_download() %}
{% if current_user.role_download() %}
{% if entry.Books.data|length %}
<div class="btn-group" role="group">
{% for format in entry.Books.data %}

View File

@ -25,7 +25,7 @@
</tr>
</tbody>
</table>
{% if g.user.role_admin() %}
{% if current_user.role_admin() %}
<h3>{{_('System Statistics')}}</h3>
<table id="libs" class="table">
<thead>

View File

@ -5,10 +5,10 @@
{% block body %}
<div class="discover">
<h2>{{_('Tasks')}}</h2>
<table class="table table-no-bordered" id="tasktable" data-url="{{ url_for('tasks.get_email_status_json') }}" data-sort-name="starttime" data-sort-order="asc" data-locale="{{ g.user.locale }}">
<table class="table table-no-bordered" id="tasktable" data-url="{{ url_for('tasks.get_email_status_json') }}" data-sort-name="starttime" data-sort-order="asc" data-locale="{{ current_user.locale }}">
<thead>
<tr>
{% if g.user.role_admin() %}
{% if current_user.role_admin() %}
<th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th>
{% endif %}
<th data-halign="right" data-align="right" data-field="taskMessage" data-sortable="true">{{_('Task')}}</th>
@ -16,7 +16,7 @@
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Run Time')}}</th>
<th data-halign="right" data-align="right" data-field="starttime" data-sortable="true" data-sort-name="id">{{_('Start Time')}}</th>
{% if g.user.role_admin() %}
{% if current_user.role_admin() %}
<th data-halign="right" data-align="right" data-formatter="TaskActions" data-switchable="false">{{_('Actions')}}</th>
{% endif %}
<th data-field="id" data-visible="false"></th>
@ -27,8 +27,8 @@
</div>
{% endblock %}
{% block modal %}
{{ delete_book() }}
{% if g.user.role_admin() %}
{{ delete_book(current_user.role_delete_books()) }}
{% if current_user.role_admin() %}
<div class="modal fade" id="cancelTaskModal" role="dialog" aria-labelledby="metaCancelTaskLabel">
<div class="modal-dialog">
<div class="modal-content">

View File

@ -5,7 +5,7 @@
<form role="form" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-md-10 col-lg-8">
{% if new_user or ( g.user and content.name != "Guest" and g.user.role_admin() ) %}
{% if new_user or ( current_user and content.name != "Guest" and current_user.role_admin() ) %}
<div class="form-group required">
<label for="name">{{_('Username')}}</label>
<input type="text" class="form-control" name="name" id="name" value="{{ content.name if content.name != None }}" autocomplete="off">
@ -15,8 +15,8 @@
<label for="email">{{_('Email')}}</label>
<input type="email" class="form-control" name="email" id="email" value="{{ content.email if content.email != None }}" autocomplete="off">
</div>
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
{% if g.user and g.user.role_admin() and not new_user and not profile and ( mail_configured and content.email if content.email != None ) %}
{% if ( current_user and current_user.role_passwd() or current_user.role_admin() ) and not content.role_anonymous() %}
{% if current_user and current_user.role_admin() and not new_user and not profile and ( mail_configured and content.email if content.email != None ) %}
<a class="btn btn-default postAction" id="resend_password" role="button" data-action="{{url_for('admin.reset_user_password', user_id = content.id) }}">{{_('Reset user Password')}}</a>
{% endif %}
<div class="form-group">
@ -83,13 +83,13 @@
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
</div>
{% if ( g.user and g.user.role_admin() and not new_user ) and not simple %}
{% if ( current_user and current_user.role_admin() and not new_user ) and not simple %}
<a href="#" id="get_user_tags" class="btn btn-default" data-id="{{content.id}}" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
<a href="#" id="get_user_column_values" data-id="{{content.id}}" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
{% endif %}
</div>
<div class="col-sm-6">
{% if g.user and g.user.role_admin() and not profile %}
{% if current_user and current_user.role_admin() and not profile %}
{% if not content.role_anonymous() %}
<div class="form-group">
<input type="checkbox" name="admin_role" id="admin_role" {% if content.role_admin() %}checked{% endif %}>
@ -143,7 +143,7 @@
{% if not profile %}
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Cancel')}}</div>
{% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
{% if current_user and current_user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>
{% endif %}
</div>

View File

@ -125,7 +125,7 @@
</div>
</div>
<table id="user-table" class="table table-no-bordered table-striped"
data-url="{{url_for('admin.list_users')}}" data-locale="{{ g.user.locale }}">
data-url="{{url_for('admin.list_users')}}" data-locale="{{ current_user.locale }}">
<thead>
<tr>
<th data-name="edit" data-buttontext="{{_('Edit User')}}" data-visible="{{visiblility.get('edit')}}" data-formatter="singleUserFormatter">{{_('Edit')}}</th>
@ -185,8 +185,8 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-select.min.js')}}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% if not current_user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + current_user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
{% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,16 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import binascii
from functools import wraps
from sqlalchemy.sql.expression import func
from werkzeug.security import check_password_hash
from flask_login import login_required, login_user
from flask import request, Response
from . import lm, ub, config, constants, services
from . import lm, ub, config, constants, services, logger
log = logger.create()
def login_required_if_no_ano(func):
@wraps(func)
@ -36,6 +36,51 @@ def login_required_if_no_ano(func):
return decorated_view
def requires_basic_auth_if_no_ano(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or auth.type != 'basic':
if config.config_anonbrowse != 1:
user = load_user_from_reverse_proxy_header(request)
if user:
return f(*args, **kwargs)
return _authenticate()
else:
return f(*args, **kwargs)
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
result, error = services.ldap.bind_user(auth.username, auth.password)
if result:
user = _fetch_user_by_name(auth.username)
login_user(user)
else:
log.error(error)
user = None
else:
user = _load_user_from_auth_header(auth.username, auth.password)
if not user:
return _authenticate()
return f(*args, **kwargs)
return decorated
def _load_user_from_auth_header(username, password):
user = _fetch_user_by_name(username)
if bool(user and check_password_hash(str(user.password), password)):
login_user(user)
return user
else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
return None
def _authenticate():
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def _fetch_user_by_name(username):
return ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
@ -43,45 +88,20 @@ def _fetch_user_by_name(username):
@lm.user_loader
def load_user(user_id):
return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
return user
@lm.request_loader
def load_user_from_request(request):
def load_user_from_reverse_proxy_header(req):
if config.config_allow_reverse_proxy_header_login:
rp_header_name = config.config_reverse_proxy_login_header_name
if rp_header_name:
rp_header_username = request.headers.get(rp_header_name)
rp_header_username = req.headers.get(rp_header_name)
if rp_header_username:
user = _fetch_user_by_name(rp_header_username)
if user:
login_user(user)
return user
return None
auth_header = request.headers.get("Authorization")
if auth_header:
user = load_user_from_auth_header(auth_header)
if user:
return user
return
def load_user_from_auth_header(header_val):
if header_val.startswith('Basic '):
header_val = header_val.replace('Basic ', '', 1)
basic_username = basic_password = '' # nosec
try:
header_val = base64.b64decode(header_val).decode('utf-8')
# Users with colon are invalid: rfc7617 page 4
basic_username = header_val.split(':', 1)[0]
basic_password = header_val.split(':', 1)[1]
except (TypeError, UnicodeDecodeError, binascii.Error):
pass
user = _fetch_user_by_name(basic_username)
if user and config.config_login_type == constants.LOGIN_LDAP and services.ldap:
if services.ldap.bind_user(str(user.password), basic_password):
return user
if user and check_password_hash(str(user.password), basic_password):
return user
return

View File

@ -31,6 +31,7 @@ from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from flask_login import login_user, logout_user, login_required, current_user
from flask_limiter import RateLimitExceeded
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
from sqlalchemy.orm.attributes import flag_modified
@ -56,7 +57,8 @@ from .kobo_sync_status import remove_synced_book
from .render_template import render_title_template
from .kobo_sync_status import change_archived_books
from . import limiter
from flask_limiter import RateLimitExceeded
from .services.worker import WorkerThread
from .tasks_status import render_task_status
feature_support = {
@ -394,7 +396,7 @@ def render_books_list(data, sort_param, book_id, page):
elif data == "archived":
return render_archived_books(page, order)
elif data == "search":
term = (request.args.get('query') or '')
term = json.loads(flask_session.get('query', ''))
offset = int(int(config.config_books_per_page) * (page - 1))
return render_search_results(term, offset, order, config.config_books_per_page)
elif data == "advsearch":

BIN
library/metadata.db Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ Flask-Babel>=0.11.1,<3.1.0
Flask-Login>=0.3.2,<0.6.3
Flask-Principal>=0.3.2,<0.5.1
backports_abc>=0.4
Flask>=1.0.2,<2.2.0
Flask>=1.0.2,<2.3.0
iso-639>=0.4.5,<0.5.0
PyPDF>=3.0.0,<3.3.0
pytz>=2016.10