mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 15:23:02 +00:00 
			
		
		
		
	Merge branch 'Develop'
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -34,3 +34,4 @@ settings.yaml | ||||
| gdrive_credentials | ||||
| client_secrets.json | ||||
| gmail.json | ||||
| /.key | ||||
|   | ||||
							
								
								
									
										2
									
								
								cps.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								cps.py
									
									
									
									
									
								
							| @@ -21,7 +21,7 @@ import os | ||||
| import sys | ||||
|  | ||||
|  | ||||
| # Add local path to sys.path so we can import cps | ||||
| # Add local path to sys.path, so we can import cps | ||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||
| sys.path.insert(0, path) | ||||
|  | ||||
|   | ||||
| @@ -41,6 +41,11 @@ from . import config_sql | ||||
| from . import cache_buster | ||||
| from . import ub, db | ||||
|  | ||||
| try: | ||||
|     from flask_limiter import Limiter | ||||
|     limiter_present = True | ||||
| except ImportError: | ||||
|     limiter_present = False | ||||
| try: | ||||
|     from flask_wtf.csrf import CSRFProtect | ||||
|     wtf_present = True | ||||
| @@ -81,7 +86,7 @@ app.config.update( | ||||
|  | ||||
| lm = MyLoginManager() | ||||
|  | ||||
| config = config_sql._ConfigSQL() | ||||
| config = config_sql.ConfigSQL() | ||||
|  | ||||
| cli_param = CliParameter() | ||||
|  | ||||
| @@ -96,33 +101,36 @@ web_server = WebServer() | ||||
|  | ||||
| updater_thread = Updater() | ||||
|  | ||||
| if limiter_present: | ||||
|     limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True) | ||||
| else: | ||||
|     limiter = None | ||||
|  | ||||
| def create_app(): | ||||
|     lm.login_view = 'web.login' | ||||
|     lm.anonymous_user = ub.Anonymous | ||||
|     lm.session_protection = 'strong' | ||||
|  | ||||
|     if csrf: | ||||
|         csrf.init_app(app) | ||||
|  | ||||
|     cli_param.init() | ||||
|  | ||||
|     ub.init_db(cli_param.settings_path, cli_param.user_credentials) | ||||
|  | ||||
|     ub.init_db(cli_param.settings_path) | ||||
|     # pylint: disable=no-member | ||||
|     config_sql.load_configuration(config, ub.session, cli_param) | ||||
|     encrypt_key, error = config_sql.get_encryption_key(os.path.dirname(cli_param.settings_path)) | ||||
|  | ||||
|     db.CalibreDB.update_config(config) | ||||
|     db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path) | ||||
|     calibre_db.init_db() | ||||
|     config_sql.load_configuration(ub.session, encrypt_key) | ||||
|     config.init_config(ub.session, encrypt_key, cli_param) | ||||
|  | ||||
|     updater_thread.init_updater(config, web_server) | ||||
|     # Perform dry run of updater and exit afterwards | ||||
|     if cli_param.dry_run: | ||||
|         updater_thread.dry_run() | ||||
|         sys.exit(0) | ||||
|     updater_thread.start() | ||||
|     if error: | ||||
|         log.error(error) | ||||
|  | ||||
|     ub.password_change(cli_param.user_credentials) | ||||
|  | ||||
|     if not limiter: | ||||
|         log.info('*** "flask-limiter" is needed for calibre-web to run. ' | ||||
|                  'Please install it using pip: "pip install flask-limiter" ***') | ||||
|         print('*** "flask-limiter" is needed for calibre-web to run. ' | ||||
|               'Please install it using pip: "pip install flask-limiter" ***') | ||||
|         web_server.stop(True) | ||||
|         sys.exit(8) | ||||
|     if sys.version_info < (3, 0): | ||||
|         log.info( | ||||
|             '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, ' | ||||
| @@ -139,6 +147,22 @@ def create_app(): | ||||
|               'Please install it using pip: "pip install flask-WTF" ***') | ||||
|         web_server.stop(True) | ||||
|         sys.exit(7) | ||||
|  | ||||
|     lm.login_view = 'web.login' | ||||
|     lm.anonymous_user = ub.Anonymous | ||||
|     lm.session_protection = 'strong' if config.config_session == 1 else "basic" | ||||
|  | ||||
|     db.CalibreDB.update_config(config) | ||||
|     db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path) | ||||
|     calibre_db.init_db() | ||||
|  | ||||
|     updater_thread.init_updater(config, web_server) | ||||
|     # Perform dry run of updater and exit afterwards | ||||
|     if cli_param.dry_run: | ||||
|         updater_thread.dry_run() | ||||
|         sys.exit(0) | ||||
|     updater_thread.start() | ||||
|  | ||||
|     for res in dependency_check() + dependency_check(True): | ||||
|         log.info('*** "{}" version does not meet the requirements. ' | ||||
|                  'Should: {}, Found: {}, please consider installing required version ***' | ||||
| @@ -150,7 +174,6 @@ def create_app(): | ||||
|     if os.environ.get('FLASK_DEBUG'): | ||||
|         cache_buster.init_cache_busting(app) | ||||
|     log.info('Starting Calibre Web...') | ||||
|  | ||||
|     Principal(app) | ||||
|     lm.init_app(app) | ||||
|     app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) | ||||
| @@ -168,9 +191,13 @@ def create_app(): | ||||
|         services.ldap.init_app(app, config) | ||||
|     if services.goodreads_support: | ||||
|         services.goodreads_support.connect(config.config_goodreads_api_key, | ||||
|                                            config.config_goodreads_api_secret, | ||||
|                                            config.config_goodreads_api_secret_e, | ||||
|                                            config.config_use_goodreads) | ||||
|     config.store_calibre_uuid(calibre_db, db.Library_Id) | ||||
|     # Configure rate limiter | ||||
|     app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter) | ||||
|     limiter.init_app(app) | ||||
|  | ||||
|     # Register scheduled tasks | ||||
|     from .schedule import register_scheduled_tasks, register_startup_tasks | ||||
|     register_scheduled_tasks(config.schedule_reconnect) | ||||
|   | ||||
							
								
								
									
										57
									
								
								cps/admin.py
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								cps/admin.py
									
									
									
									
									
								
							| @@ -22,7 +22,6 @@ | ||||
|  | ||||
| import os | ||||
| import re | ||||
| import base64 | ||||
| import json | ||||
| import operator | ||||
| import time | ||||
| @@ -104,7 +103,6 @@ def before_request(): | ||||
|     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.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','') | ||||
|     g.allow_registration = config.config_public_reg | ||||
|     g.allow_anonymous = config.config_anonbrowse | ||||
| @@ -116,6 +114,7 @@ def before_request(): | ||||
|                                  'admin.simulatedbchange', | ||||
|                                  'admin.db_configuration', | ||||
|                                  'web.login', | ||||
|                                  'web.login_post', | ||||
|                                  'web.logout', | ||||
|                                  'admin.load_dialogtexts', | ||||
|                                  'admin.ajax_pathchooser'): | ||||
| @@ -214,12 +213,12 @@ def admin(): | ||||
|             commit = version['version'] | ||||
|  | ||||
|     all_user = ub.session.query(ub.User).all() | ||||
|     email_settings = config.get_mail_settings() | ||||
|     # email_settings = mail_config.get_mail_settings() | ||||
|     schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short") | ||||
|     t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60) | ||||
|     schedule_duration = format_timedelta(t, threshold=.99) | ||||
|  | ||||
|     return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit, | ||||
|     return render_title_template("admin.html", allUser=all_user, config=config, commit=commit, | ||||
|                                  feature_support=feature_support, schedule_time=schedule_time, | ||||
|                                  schedule_duration=schedule_duration, | ||||
|                                  title=_("Admin page"), page="admin") | ||||
| @@ -1084,7 +1083,7 @@ def _config_checkbox_int(to_save, x): | ||||
|  | ||||
|  | ||||
| def _config_string(to_save, x): | ||||
|     return config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) | ||||
|     return config.set_from_dictionary(to_save, x, lambda y: y.strip().strip(u'\u200B\u200C\u200D\ufeff') if y else y) | ||||
|  | ||||
|  | ||||
| def _configuration_gdrive_helper(to_save): | ||||
| @@ -1173,9 +1172,9 @@ def _configuration_ldap_helper(to_save): | ||||
|     reboot_required |= _config_string(to_save, "config_ldap_cert_path") | ||||
|     reboot_required |= _config_string(to_save, "config_ldap_key_path") | ||||
|     _config_string(to_save, "config_ldap_group_name") | ||||
|     if to_save.get("config_ldap_serv_password", "") != "": | ||||
|     if to_save.get("config_ldap_serv_password_e", "") != "": | ||||
|         reboot_required |= 1 | ||||
|         config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') | ||||
|         config.set_from_dictionary(to_save, "config_ldap_serv_password_e") | ||||
|     config.save() | ||||
|  | ||||
|     if not config.config_ldap_provider_url \ | ||||
| @@ -1187,7 +1186,7 @@ def _configuration_ldap_helper(to_save): | ||||
|  | ||||
|     if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: | ||||
|         if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: | ||||
|             if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password): | ||||
|             if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password_e): | ||||
|                 return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password')) | ||||
|         else: | ||||
|             if not config.config_ldap_serv_username: | ||||
| @@ -1255,7 +1254,7 @@ def new_user(): | ||||
|                                  kobo_support=kobo_support, registered_oauth=oauth_check) | ||||
|  | ||||
|  | ||||
| @admi.route("/admin/mailsettings") | ||||
| @admi.route("/admin/mailsettings", methods=["GET"]) | ||||
| @login_required | ||||
| @admin_required | ||||
| def edit_mailsettings(): | ||||
| @@ -1288,7 +1287,7 @@ def update_mailsettings(): | ||||
|     else: | ||||
|         _config_int(to_save, "mail_port") | ||||
|         _config_int(to_save, "mail_use_ssl") | ||||
|         _config_string(to_save, "mail_password") | ||||
|         _config_string(to_save, "mail_password_e") | ||||
|         _config_int(to_save, "mail_size", lambda y: int(y) * 1024 * 1024) | ||||
|         config.mail_server = to_save.get('mail_server', "").strip() | ||||
|         config.mail_from = to_save.get('mail_from', "").strip() | ||||
| @@ -1771,10 +1770,10 @@ def _configuration_update_helper(): | ||||
|         # Goodreads configuration | ||||
|         _config_checkbox(to_save, "config_use_goodreads") | ||||
|         _config_string(to_save, "config_goodreads_api_key") | ||||
|         _config_string(to_save, "config_goodreads_api_secret") | ||||
|         _config_string(to_save, "config_goodreads_api_secret_e") | ||||
|         if services.goodreads_support: | ||||
|             services.goodreads_support.connect(config.config_goodreads_api_key, | ||||
|                                                config.config_goodreads_api_secret, | ||||
|                                                config.config_goodreads_api_secret_e, | ||||
|                                                config.config_use_goodreads) | ||||
|  | ||||
|         _config_int(to_save, "config_updatechannel") | ||||
| @@ -1787,10 +1786,25 @@ def _configuration_update_helper(): | ||||
|         if config.config_login_type == constants.LOGIN_OAUTH: | ||||
|             reboot_required |= _configuration_oauth_helper(to_save) | ||||
|  | ||||
|         # logfile configuration | ||||
|         reboot, message = _configuration_logfile_helper(to_save) | ||||
|         if message: | ||||
|             return message | ||||
|         reboot_required |= reboot | ||||
|  | ||||
|         # security configuration | ||||
|         _config_checkbox(to_save, "config_password_policy") | ||||
|         _config_checkbox(to_save, "config_password_number") | ||||
|         _config_checkbox(to_save, "config_password_lower") | ||||
|         _config_checkbox(to_save, "config_password_upper") | ||||
|         _config_checkbox(to_save, "config_password_special") | ||||
|         if 0 < int(to_save.get("config_password_min_length", "0")) < 41: | ||||
|             _config_int(to_save, "config_password_min_length") | ||||
|         else: | ||||
|             return _configuration_result(_('Password length has to be between 1 and 40')) | ||||
|         reboot_required |= _config_int(to_save, "config_session") | ||||
|         reboot_required |= _config_checkbox(to_save, "config_ratelimiter") | ||||
|  | ||||
|         # Rarfile Content configuration | ||||
|         _config_string(to_save, "config_rarfile_location") | ||||
|         if "config_rarfile_location" in to_save: | ||||
| @@ -1859,11 +1873,11 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): | ||||
|         content.sidebar_view |= constants.DETAIL_RANDOM | ||||
|  | ||||
|     content.role = constants.selected_roles(to_save) | ||||
|     content.password = generate_password_hash(to_save["password"]) | ||||
|     try: | ||||
|         if not to_save["name"] or not to_save["email"] or not to_save["password"]: | ||||
|             log.info("Missing entries on new user") | ||||
|             raise Exception(_("Oops! Please complete all fields.")) | ||||
|         content.password = generate_password_hash(helper.valid_password(to_save.get("password", ""))) | ||||
|         content.email = check_email(to_save["email"]) | ||||
|         # Query username, if not existing, change | ||||
|         content.name = check_username(to_save["name"]) | ||||
| @@ -1947,14 +1961,6 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): | ||||
|             log.warning("No admin user remaining, can't remove admin role from {}".format(content.name)) | ||||
|             flash(_("No admin user remaining, can't remove admin role"), category="error") | ||||
|             return redirect(url_for('admin.admin')) | ||||
|         if to_save.get("password"): | ||||
|             content.password = generate_password_hash(to_save["password"]) | ||||
|         anonymous = content.is_anonymous | ||||
|         content.role = constants.selected_roles(to_save) | ||||
|         if anonymous: | ||||
|             content.role |= constants.ROLE_ANONYMOUS | ||||
|         else: | ||||
|             content.role &= ~constants.ROLE_ANONYMOUS | ||||
|  | ||||
|         val = [int(k[5:]) for k in to_save if k.startswith('show_')] | ||||
|         sidebar, __ = get_sidebar_config() | ||||
| @@ -1982,6 +1988,15 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): | ||||
|         if to_save.get("locale"): | ||||
|             content.locale = to_save["locale"] | ||||
|         try: | ||||
|             anonymous = content.is_anonymous | ||||
|             content.role = constants.selected_roles(to_save) | ||||
|             if anonymous: | ||||
|                 content.role |= constants.ROLE_ANONYMOUS | ||||
|             else: | ||||
|                 content.role &= ~constants.ROLE_ANONYMOUS | ||||
|                 if to_save.get("password", ""): | ||||
|                     content.password = generate_password_hash(helper.valid_password(to_save.get["password"])) | ||||
|  | ||||
|             new_email = valid_email(to_save.get("email", content.email)) | ||||
|             if not new_email: | ||||
|                 raise Exception(_("Email can't be empty and has to be a valid Email")) | ||||
|   | ||||
| @@ -23,6 +23,10 @@ import json | ||||
| from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON | ||||
| from sqlalchemy.exc import OperationalError | ||||
| from sqlalchemy.sql.expression import text | ||||
| from sqlalchemy import exists | ||||
| from cryptography.fernet import Fernet | ||||
| import cryptography.exceptions | ||||
| from base64 import urlsafe_b64decode | ||||
| try: | ||||
|     # Compatibility with sqlalchemy 2.0 | ||||
|     from sqlalchemy.orm import declarative_base | ||||
| @@ -56,7 +60,8 @@ class _Settings(_Base): | ||||
|     mail_port = Column(Integer, default=25) | ||||
|     mail_use_ssl = Column(SmallInteger, default=0) | ||||
|     mail_login = Column(String, default='mail@example.com') | ||||
|     mail_password = Column(String, default='mypassword') | ||||
|     mail_password_e = Column(String) | ||||
|     mail_password = Column(String) | ||||
|     mail_from = Column(String, default='automailer <mail@example.com>') | ||||
|     mail_size = Column(Integer, default=25*1024*1024) | ||||
|     mail_server_type = Column(SmallInteger, default=0) | ||||
| @@ -75,7 +80,6 @@ class _Settings(_Base): | ||||
|     config_authors_max = Column(Integer, default=0) | ||||
|     config_read_column = Column(Integer, default=0) | ||||
|     config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines|Le|La|Les|L\'|Un|Une)\s+')     | ||||
|     # config_mature_content_tags = Column(String, default='') | ||||
|     config_theme = Column(Integer, default=0) | ||||
|  | ||||
|     config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL) | ||||
| @@ -107,6 +111,7 @@ class _Settings(_Base): | ||||
|  | ||||
|     config_use_goodreads = Column(Boolean, default=False) | ||||
|     config_goodreads_api_key = Column(String) | ||||
|     config_goodreads_api_secret_e = Column(String) | ||||
|     config_goodreads_api_secret = Column(String) | ||||
|     config_register_email = Column(Boolean, default=False) | ||||
|     config_login_type = Column(Integer, default=0) | ||||
| @@ -117,7 +122,8 @@ class _Settings(_Base): | ||||
|     config_ldap_port = Column(SmallInteger, default=389) | ||||
|     config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE) | ||||
|     config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org') | ||||
|     config_ldap_serv_password = Column(String, default="") | ||||
|     config_ldap_serv_password_e = Column(String) | ||||
|     config_ldap_serv_password = Column(String) | ||||
|     config_ldap_encryption = Column(SmallInteger, default=0) | ||||
|     config_ldap_cacert_path = Column(String, default="") | ||||
|     config_ldap_cert_path = Column(String, default="") | ||||
| @@ -148,23 +154,33 @@ class _Settings(_Base): | ||||
|     schedule_generate_series_covers = Column(Boolean, default=False) | ||||
|     schedule_reconnect = Column(Boolean, default=False) | ||||
|  | ||||
|     config_password_policy = Column(Boolean, default=True) | ||||
|     config_password_min_length = Column(Integer, default=8) | ||||
|     config_password_number = Column(Boolean, default=True) | ||||
|     config_password_lower = Column(Boolean, default=True) | ||||
|     config_password_upper = Column(Boolean, default=True) | ||||
|     config_password_special = Column(Boolean, default=True) | ||||
|     config_session = Column(Integer, default=1) | ||||
|     config_ratelimiter = Column(Boolean, default=True) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return self.__class__.__name__ | ||||
|  | ||||
|  | ||||
| # Class holds all application specific settings in calibre-web | ||||
| class _ConfigSQL(object): | ||||
| class ConfigSQL(object): | ||||
|     # pylint: disable=no-member | ||||
|     def __init__(self): | ||||
|         pass | ||||
|         self.__dict__["dirty"] = list() | ||||
|  | ||||
|     def init_config(self, session, cli): | ||||
|     def init_config(self, session, secret_key, cli): | ||||
|         self._session = session | ||||
|         self._settings = None | ||||
|         self.db_configured = None | ||||
|         self.config_calibre_dir = None | ||||
|         self.load() | ||||
|         self._fernet = Fernet(secret_key) | ||||
|         self.cli = cli | ||||
|         self.load() | ||||
|  | ||||
|         change = False | ||||
|         if self.config_converterpath == None:  # pylint: disable=access-member-before-definition | ||||
| @@ -293,10 +309,10 @@ class _ConfigSQL(object): | ||||
|         setattr(self, field, new_value) | ||||
|         return True | ||||
|  | ||||
|     def toDict(self): | ||||
|     def to_dict(self): | ||||
|         storage = {} | ||||
|         for k, v in self.__dict__.items(): | ||||
|             if k[0] != '_' and not k.endswith("password") and not k.endswith("secret") and not k == "cli": | ||||
|             if k[0] != '_' and not k.endswith("_e") and not k == "cli": | ||||
|                 storage[k] = v | ||||
|         return storage | ||||
|  | ||||
| @@ -310,6 +326,12 @@ class _ConfigSQL(object): | ||||
|                     column = s.__class__.__dict__.get(k) | ||||
|                     if column.default is not None: | ||||
|                         v = column.default.arg | ||||
|                 if k.endswith("_e") and v is not None: | ||||
|                     try: | ||||
|                         setattr(self, k, self._fernet.decrypt(v).decode()) | ||||
|                     except cryptography.fernet.InvalidToken: | ||||
|                         setattr(self, k, "") | ||||
|                 else: | ||||
|                     setattr(self, k, v) | ||||
|  | ||||
|         have_metadata_db = bool(self.config_calibre_dir) | ||||
| @@ -332,16 +354,20 @@ class _ConfigSQL(object): | ||||
|             except OperationalError as e: | ||||
|                 log.error('Database error: %s', e) | ||||
|                 self._session.rollback() | ||||
|         self.__dict__["dirty"] = list() | ||||
|  | ||||
|     def save(self): | ||||
|         """Apply all configuration values to the underlying storage.""" | ||||
|         s = self._read_from_storage()  # type: _Settings | ||||
|  | ||||
|         for k, v in self.__dict__.items(): | ||||
|         for k in self.dirty: | ||||
|             if k[0] == '_': | ||||
|                 continue | ||||
|             if hasattr(s, k): | ||||
|                 setattr(s, k, v) | ||||
|                 if k.endswith("_e"): | ||||
|                     setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode())) | ||||
|                 else: | ||||
|                     setattr(s, k, self.__dict__[k]) | ||||
|  | ||||
|         log.debug("_ConfigSQL updating storage") | ||||
|         self._session.merge(s) | ||||
| @@ -357,7 +383,6 @@ class _ConfigSQL(object): | ||||
|             log.error(error) | ||||
|         log.warning("invalidating configuration") | ||||
|         self.db_configured = False | ||||
|         # self.config_calibre_dir = None | ||||
|         self.save() | ||||
|  | ||||
|     def store_calibre_uuid(self, calibre_db, Library_table): | ||||
| @@ -369,8 +394,40 @@ class _ConfigSQL(object): | ||||
|         except AttributeError: | ||||
|             pass | ||||
|  | ||||
|     def __setattr__(self, attr_name, attr_value): | ||||
|         super().__setattr__(attr_name, attr_value) | ||||
|         self.__dict__["dirty"].append(attr_name) | ||||
|  | ||||
| def _migrate_table(session, orm_class): | ||||
|  | ||||
| def _encrypt_fields(session, secret_key): | ||||
|     try: | ||||
|         session.query(exists().where(_Settings.mail_password_e)).scalar() | ||||
|     except OperationalError: | ||||
|         with session.bind.connect() as conn: | ||||
|             conn.execute("ALTER TABLE settings ADD column 'mail_password_e' String") | ||||
|             conn.execute("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String") | ||||
|             conn.execute("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String") | ||||
|         session.commit() | ||||
|         crypter = Fernet(secret_key) | ||||
|         settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret, | ||||
|                                  _Settings.config_ldap_serv_password).first() | ||||
|         if settings.mail_password: | ||||
|             session.query(_Settings).update( | ||||
|                 {_Settings.mail_password_e: crypter.encrypt(settings.mail_password.encode())}) | ||||
|         if settings.config_goodreads_api_secret: | ||||
|             session.query(_Settings).update( | ||||
|                 {_Settings.config_goodreads_api_secret_e: | ||||
|                      crypter.encrypt(settings.config_goodreads_api_secret.encode())}) | ||||
|         if settings.config_ldap_serv_password: | ||||
|             session.query(_Settings).update( | ||||
|                 {_Settings.config_ldap_serv_password_e: | ||||
|                      crypter.encrypt(settings.config_ldap_serv_password.encode())}) | ||||
|         session.commit() | ||||
|  | ||||
|  | ||||
| def _migrate_table(session, orm_class, secret_key=None): | ||||
|     if secret_key: | ||||
|         _encrypt_fields(session, secret_key) | ||||
|     changed = False | ||||
|  | ||||
|     for column_name, column in orm_class.__dict__.items(): | ||||
| @@ -446,22 +503,18 @@ def autodetect_kepubify_binary(): | ||||
|     return "" | ||||
|  | ||||
|  | ||||
| def _migrate_database(session): | ||||
| def _migrate_database(session, secret_key): | ||||
|     # make sure the table is created, if it does not exist | ||||
|     _Base.metadata.create_all(session.bind) | ||||
|     _migrate_table(session, _Settings) | ||||
|     _migrate_table(session, _Settings, secret_key) | ||||
|     _migrate_table(session, _Flask_Settings) | ||||
|  | ||||
|  | ||||
| def load_configuration(conf, session, cli): | ||||
|     _migrate_database(session) | ||||
|  | ||||
| def load_configuration(session, secret_key): | ||||
|     _migrate_database(session, secret_key) | ||||
|     if not session.query(_Settings).count(): | ||||
|         session.add(_Settings()) | ||||
|         session.commit() | ||||
|     # conf = _ConfigSQL() | ||||
|     conf.init_config(session, cli) | ||||
|     # return conf | ||||
|  | ||||
|  | ||||
| def get_flask_session_key(_session): | ||||
| @@ -471,3 +524,25 @@ def get_flask_session_key(_session): | ||||
|         _session.add(flask_settings) | ||||
|         _session.commit() | ||||
|     return flask_settings.flask_session_key | ||||
|  | ||||
|  | ||||
| def get_encryption_key(key_path): | ||||
|     key_file = os.path.join(key_path, ".key") | ||||
|     generate = True | ||||
|     error = "" | ||||
|     if os.path.exists(key_file) and  os.path.getsize(key_file) > 32: | ||||
|         with open(key_file, "rb") as f: | ||||
|             key = f.read() | ||||
|         try: | ||||
|             urlsafe_b64decode(key) | ||||
|             generate = False | ||||
|         except ValueError: | ||||
|             pass | ||||
|     if generate: | ||||
|         key = Fernet.generate_key() | ||||
|         try: | ||||
|             with open(key_file, "wb") as f: | ||||
|                 f.write(key) | ||||
|         except PermissionError as e: | ||||
|             error = e | ||||
|     return key, error | ||||
|   | ||||
| @@ -65,7 +65,7 @@ def send_debug(): | ||||
|             file_list.remove(element) | ||||
|     memory_zip = BytesIO() | ||||
|     with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf: | ||||
|         zf.writestr('settings.txt', json.dumps(config.toDict(), sort_keys=True, indent=2)) | ||||
|         zf.writestr('settings.txt', json.dumps(config.to_dict(), sort_keys=True, indent=2)) | ||||
|         zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder)) | ||||
|         for fp in file_list: | ||||
|             zf.write(fp, os.path.basename(fp)) | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
| #  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| import os | ||||
| import random | ||||
| import io | ||||
| import mimetypes | ||||
| import re | ||||
| @@ -612,7 +613,7 @@ def reset_password(user_id): | ||||
|     if not config.get_mail_server_configured(): | ||||
|         return 2, None | ||||
|     try: | ||||
|         password = generate_random_password() | ||||
|         password = generate_random_password(config.config_password_min_length) | ||||
|         existing_user.password = generate_password_hash(password) | ||||
|         ub.session.commit() | ||||
|         send_registration_mail(existing_user.email, existing_user.name, password, True) | ||||
| @@ -621,11 +622,35 @@ def reset_password(user_id): | ||||
|         ub.session.rollback() | ||||
|         return 0, None | ||||
|  | ||||
| def generate_random_password(min_length): | ||||
|     min_length = max(8, min_length) - 4 | ||||
|     random_source = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" | ||||
|     # select 1 lowercase | ||||
|     s = "abcdefghijklmnopqrstuvwxyz" | ||||
|     password = [s[c % len(s)] for c in os.urandom(1)] | ||||
|     # select 1 uppercase | ||||
|     s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||||
|     password.extend([s[c % len(s)] for c in os.urandom(1)]) | ||||
|     # select 1 digit | ||||
|     s = "01234567890" | ||||
|     password.extend([s[c % len(s)] for c in os.urandom(1)]) | ||||
|     # select 1 special symbol | ||||
|     s = "!@#$%&*()?" | ||||
|     password.extend([s[c % len(s)] for c in os.urandom(1)]) | ||||
|  | ||||
| def generate_random_password(): | ||||
|     # generate other characters | ||||
|     password.extend([random_source[c % len(random_source)] for c in os.urandom(min_length)]) | ||||
|  | ||||
|     # password_list = list(password) | ||||
|     # shuffle all characters | ||||
|     random.SystemRandom().shuffle(password) | ||||
|     return ''.join(password) | ||||
|  | ||||
|  | ||||
| '''def generate_random_password(min_length): | ||||
|     s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" | ||||
|     passlen = 8 | ||||
|     return "".join(s[c % len(s)] for c in os.urandom(passlen)) | ||||
|     passlen = min_length | ||||
|     return "".join(s[c % len(s)] for c in os.urandom(passlen))''' | ||||
|  | ||||
|  | ||||
| def uniq(inpt): | ||||
| @@ -664,6 +689,23 @@ def valid_email(email): | ||||
|             raise Exception(_("Invalid Email address format")) | ||||
|     return email | ||||
|  | ||||
| def valid_password(check_password): | ||||
|     if config.config_password_policy: | ||||
|         verify = "" | ||||
|         if config.config_password_min_length > 0: | ||||
|             verify += "^(?=.{" + str(config.config_password_min_length) + ",}$)" | ||||
|         if config.config_password_number: | ||||
|             verify += "(?=.*?\d)" | ||||
|         if config.config_password_lower: | ||||
|             verify += "(?=.*?[a-z])" | ||||
|         if config.config_password_upper: | ||||
|             verify += "(?=.*?[A-Z])" | ||||
|         if config.config_password_special: | ||||
|             verify += "(?=.*?[^A-Za-z\s0-9])" | ||||
|         match = re.match(verify, check_password) | ||||
|         if not match: | ||||
|             raise Exception(_("Password doesn't comply with password validation rules")) | ||||
|     return check_password | ||||
| # ################################# External interface ################################# | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -64,11 +64,12 @@ from datetime import datetime | ||||
| from os import urandom | ||||
| from functools import wraps | ||||
|  | ||||
| from flask import g, Blueprint, url_for, abort, request | ||||
| from flask import g, Blueprint, abort, request | ||||
| from flask_login import login_user, current_user, login_required | ||||
| from flask_babel import gettext as _ | ||||
| from flask_limiter import RateLimitExceeded | ||||
|  | ||||
| from . import logger, config, calibre_db, db, helper, ub, lm | ||||
| from . import logger, config, calibre_db, db, helper, ub, lm, limiter | ||||
| from .render_template import render_title_template | ||||
|  | ||||
| log = logger.create() | ||||
| @@ -151,6 +152,10 @@ def requires_kobo_auth(f): | ||||
|     def inner(*args, **kwargs): | ||||
|         auth_token = get_auth_token() | ||||
|         if auth_token is not None: | ||||
|             try: | ||||
|                 limiter.check() | ||||
|             except RateLimitExceeded: | ||||
|                 return abort(429) | ||||
|             user = ( | ||||
|                 ub.session.query(ub.User) | ||||
|                 .join(ub.RemoteAuthToken) | ||||
| @@ -159,6 +164,7 @@ def requires_kobo_auth(f): | ||||
|             ) | ||||
|             if user is not None: | ||||
|                 login_user(user) | ||||
|                 [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] | ||||
|                 return f(*args, **kwargs) | ||||
|         log.debug("Received Kobo request without a recognizable auth token.") | ||||
|         return abort(401) | ||||
|   | ||||
							
								
								
									
										10
									
								
								cps/main.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cps/main.py
									
									
									
									
									
								
							| @@ -18,9 +18,14 @@ | ||||
|  | ||||
| import sys | ||||
|  | ||||
| from . import create_app | ||||
| from . import create_app, limiter | ||||
| from .jinjia import jinjia | ||||
| from .remotelogin import remotelogin | ||||
| from flask import request | ||||
|  | ||||
|  | ||||
| def request_username(): | ||||
|     return request.authorization.username | ||||
|  | ||||
| def main(): | ||||
|     app = create_app() | ||||
| @@ -39,6 +44,7 @@ def main(): | ||||
|     try: | ||||
|         from .kobo import kobo, get_kobo_activated | ||||
|         from .kobo_auth import kobo_auth | ||||
|         from flask_limiter.util import get_remote_address | ||||
|         kobo_available = get_kobo_activated() | ||||
|     except (ImportError, AttributeError):  # Catch also error for not installed flask-WTF (missing csrf decorator) | ||||
|         kobo_available = False | ||||
| @@ -56,6 +62,7 @@ def main(): | ||||
|     app.register_blueprint(tasks) | ||||
|     app.register_blueprint(web) | ||||
|     app.register_blueprint(opds) | ||||
|     limiter.limit("3/minute",key_func=request_username)(opds) | ||||
|     app.register_blueprint(jinjia) | ||||
|     app.register_blueprint(about) | ||||
|     app.register_blueprint(shelf) | ||||
| @@ -67,6 +74,7 @@ def main(): | ||||
|     if kobo_available: | ||||
|         app.register_blueprint(kobo) | ||||
|         app.register_blueprint(kobo_auth) | ||||
|         limiter.limit("3/minute", key_func=get_remote_address)(kobo) | ||||
|     if oauth_available: | ||||
|         app.register_blueprint(oauth) | ||||
|     success = web_server.start() | ||||
|   | ||||
| @@ -27,8 +27,7 @@ from .tasks.metadata_backup import TaskBackupMetadata | ||||
|  | ||||
| def get_scheduled_tasks(reconnect=True): | ||||
|     tasks = list() | ||||
|     # config.schedule_reconnect or | ||||
|     # Reconnect Calibre database (metadata.db) | ||||
|     # Reconnect Calibre database (metadata.db) based on config.schedule_reconnect | ||||
|     if reconnect: | ||||
|         tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False]) | ||||
|  | ||||
|   | ||||
| @@ -22,7 +22,6 @@ import errno | ||||
| import signal | ||||
| import socket | ||||
| import subprocess  # nosec | ||||
| from .services.background_scheduler import BackgroundScheduler | ||||
|  | ||||
| try: | ||||
|     from gevent.pywsgi import WSGIServer | ||||
| @@ -270,6 +269,7 @@ class WebServer(object): | ||||
|  | ||||
|     @staticmethod | ||||
|     def shutdown_scheduler(): | ||||
|         from .services.background_scheduler import BackgroundScheduler | ||||
|         scheduler = BackgroundScheduler() | ||||
|         if scheduler: | ||||
|             scheduler.scheduler.shutdown() | ||||
|   | ||||
| @@ -44,15 +44,15 @@ def init_app(app, config): | ||||
|         app.config['LDAP_SCHEMA'] = 'ldap' | ||||
|     if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: | ||||
|         if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: | ||||
|             if config.config_ldap_serv_password is None: | ||||
|                 config.config_ldap_serv_password = '' | ||||
|             app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) | ||||
|             if config.config_ldap_serv_password_e is None: | ||||
|                 config.config_ldap_serv_password_e = '' | ||||
|             app.config['LDAP_PASSWORD'] = config.config_ldap_serv_password_e | ||||
|         else: | ||||
|             app.config['LDAP_PASSWORD'] = base64.b64decode("") | ||||
|             app.config['LDAP_PASSWORD'] = "" | ||||
|         app.config['LDAP_USERNAME'] = config.config_ldap_serv_username | ||||
|     else: | ||||
|         app.config['LDAP_USERNAME'] = "" | ||||
|         app.config['LDAP_PASSWORD'] = base64.b64decode("") | ||||
|         app.config['LDAP_PASSWORD'] = "" | ||||
|     if bool(config.config_ldap_cert_path): | ||||
|         app.config['LDAP_CUSTOM_OPTIONS'].update({ | ||||
|             pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND, | ||||
|   | ||||
| @@ -434,3 +434,7 @@ div.log { | ||||
| #detailcover:-moz-full-screen { cursor:zoom-out; border: 0; } | ||||
| #detailcover:-ms-fullscreen { cursor:zoom-out; border: 0; } | ||||
| #detailcover:fullscreen { cursor:zoom-out; border: 0; } | ||||
|  | ||||
| .error-list { | ||||
|     margin-top: 5px; | ||||
|   } | ||||
|   | ||||
							
								
								
									
										1
									
								
								cps/static/js/libs/pwstrength/i18next.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cps/static/js/libs/pwstrength/i18next.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cps/static/js/libs/pwstrength/i18nextHttpBackend.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cps/static/js/libs/pwstrength/i18nextHttpBackend.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/ar.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/ar.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "كلمة المرور قصيرة جداً", | ||||
|     "wordMaxLength": "كلمة المرور طويلة جدا", | ||||
|     "wordInvalidChar": "تحتوي كلمة المرور على رموز غير صالحة", | ||||
|     "wordNotEmail": "لا تستخدم بريدك الإلكتروني ككلمة مرور", | ||||
|     "wordSimilarToUsername": "لا يمكن ان تحتوي كلمة المرور على إسم المستخدم", | ||||
|     "wordTwoCharacterClasses": "إستخدم فئات أحرف مختلفة", | ||||
|     "wordRepetitions": "تكرارات كثيرة", | ||||
|     "wordSequences": "تحتوي كلمة المرور على أنماط متتابعة", | ||||
|     "errorList": "الأخطاء:", | ||||
|     "veryWeak": "ضعيفة جداً", | ||||
|     "weak": "ضعيفة", | ||||
|     "normal": "عادية", | ||||
|     "medium": "متوسطة", | ||||
|     "strong": "قوية", | ||||
|     "veryStrong": "قوية جداً" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/cs.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/cs.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|   "wordMinLength": "Vaše heslo je příliš krátké", | ||||
|   "wordMaxLength": "Vaše heslo je příliš dlouhé", | ||||
|   "wordInvalidChar": "Vaše heslo obsahuje neplatný znak", | ||||
|   "wordNotEmail": "Nepoužívejte Váš email jako Vaše heslo", | ||||
|   "wordSimilarToUsername": "Vaše heslo nesmí obsahovat přihlašovací jméno", | ||||
|   "wordTwoCharacterClasses": "Použijte různé druhy znaků", | ||||
|   "wordRepetitions": "Příliš mnoho opakování", | ||||
|   "wordSequences": "Vaše heslo obsahuje postupnost", | ||||
|   "errorList": "Chyby:", | ||||
|   "veryWeak": "Velmi slabé", | ||||
|   "weak": "Slabé", | ||||
|   "normal": "Normální", | ||||
|   "medium": "Středně silné", | ||||
|   "strong": "Silné", | ||||
|   "veryStrong": "Velmi silné" | ||||
| } | ||||
							
								
								
									
										21
									
								
								cps/static/js/libs/pwstrength/locales/de.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								cps/static/js/libs/pwstrength/locales/de.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|     "wordMinLength": "Das Passwort ist zu kurz", | ||||
|     "wordMaxLength": "Das Passwort ist zu lang", | ||||
|     "wordInvalidChar": "Das Passwort enthält ein ungültiges Zeichen", | ||||
|     "wordNotEmail": "Das Passwort darf die E-Mail Adresse nicht enthalten", | ||||
|     "wordSimilarToUsername": "Das Passwort darf den Benutzernamen nicht enthalten", | ||||
|     "wordTwoCharacterClasses": "Bitte Buchstaben und Ziffern verwenden", | ||||
|     "wordRepetitions": "Zu viele Wiederholungen", | ||||
|     "wordSequences": "Das Passwort enthält Buchstabensequenzen", | ||||
|     "wordLowercase": "Bitte mindestens einen Kleinbuchstaben verwenden", | ||||
|     "wordUppercase": "Bitte mindestens einen Großbuchstaben verwenden", | ||||
|     "wordOneNumber": "Bitte mindestens eine Ziffern verwenden", | ||||
|     "wordOneSpecialChar": "Bitte mindestens ein Sonderzeichen verwenden", | ||||
|     "errorList": "Fehler:", | ||||
|     "veryWeak": "Sehr schwach", | ||||
|     "weak": "Schwach", | ||||
|     "normal": "Normal", | ||||
|     "medium": "Mittel", | ||||
|     "strong": "Stark", | ||||
|     "veryStrong": "Sehr stark" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/el.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/el.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Ο κωδικός πρόσβασης δεν έχει τον ελάχιστο αριθμό χαρακτήρων", | ||||
|     "wordMaxLength": "Ο κωδικός πρόσβασής σας είναι πολύ μεγάλος", | ||||
|     "wordInvalidChar": "Ο κωδικός πρόσβασής σας περιέχει έναν μη έγκυρο χαρακτήρα", | ||||
|     "wordNotEmail": "Μη χρησιμοποιείτε το email ως κωδικό", | ||||
|     "wordSimilarToUsername": "Ο κωδικός πρόσβασης δεν πρέπει να περιέχει το username", | ||||
|     "wordTwoCharacterClasses": "Χρησιμοποιήστε διαφορετικές κλάσεις χαρακτήρων", | ||||
|     "wordRepetitions": "Πολλές επαναλήψεις", | ||||
|     "wordSequences": "Ο κωδικός πρόσβασης περιέχει επαναλήψεις", | ||||
|     "errorList": "Σφάλματα:", | ||||
|     "veryWeak": "Πολύ Αδύνατος", | ||||
|     "weak": "Αδύνατος", | ||||
|     "normal": "Κανονικός", | ||||
|     "medium": "Μέτριος", | ||||
|     "strong": "Δυνατός", | ||||
|     "veryStrong": "Πολύ Δυνατός" | ||||
| } | ||||
							
								
								
									
										21
									
								
								cps/static/js/libs/pwstrength/locales/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								cps/static/js/libs/pwstrength/locales/en.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|     "wordMinLength": "Your password is too short", | ||||
|     "wordMaxLength": "Your password is too long", | ||||
|     "wordInvalidChar": "Your password contains an invalid character", | ||||
|     "wordNotEmail": "Do not use your email as your password", | ||||
|     "wordSimilarToUsername": "Your password cannot contain your username", | ||||
|     "wordTwoCharacterClasses": "Use different character classes", | ||||
|     "wordRepetitions": "Too many repetitions", | ||||
|     "wordSequences": "Your password contains sequences", | ||||
|     "wordLowercase": "Use at least one lowercase character", | ||||
|     "wordUppercase": "Use at least one uppercase character", | ||||
|     "wordOneNumber": "Use at least one number", | ||||
|     "wordOneSpecialChar": "Use at least one special character", | ||||
|     "errorList": "Errors:", | ||||
|     "veryWeak": "Very Weak", | ||||
|     "weak": "Weak", | ||||
|     "normal": "Normal", | ||||
|     "medium": "Medium", | ||||
|     "strong": "Strong", | ||||
|     "veryStrong": "Very Strong" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/eo.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/eo.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Via pasvorto estas tro mallonga", | ||||
|     "wordMaxLength": "Via pasvorto estas tro longa", | ||||
|     "wordInvalidChar": "Via pasvorto enhavas nevalidan karaktero", | ||||
|     "wordNotEmail": "Ne uzu vian retpoŝtadreson kiel la pasvorton", | ||||
|     "wordSimilarToUsername": "Via pasvorto enhavas vian uzanto-nomon", | ||||
|     "wordTwoCharacterClasses": "Uzu signojn de diversaj tipoj (ekz., literoj kaj ciferoj)", | ||||
|     "wordRepetitions": "Tro multaj ripetiĝantaj signoj", | ||||
|     "wordSequences": "Via pasvorto enhavas simplan sinsekvon de signoj", | ||||
|     "errorList": "Eraroj:", | ||||
|     "veryWeak": "Trosimpla", | ||||
|     "weak": "Malforta", | ||||
|     "normal": "Mezforta", | ||||
|     "medium": "Akceptebla", | ||||
|     "strong": "Forta", | ||||
|     "veryStrong": "Elstare Forta" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/es.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/es.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Tu contraseña es demasiado corta", | ||||
|     "wordMaxLength": "Tu contraseña es muy larga", | ||||
|     "wordInvalidChar": "Tu contraseña contiene un carácter no válido", | ||||
|     "wordNotEmail": "No uses tu email como tu contraseña", | ||||
|     "wordSimilarToUsername": "Tu contraseña no puede contener tu nombre de usuario", | ||||
|     "wordTwoCharacterClasses": "Mezcla diferentes clases de caracteres", | ||||
|     "wordRepetitions": "Demasiadas repeticiones", | ||||
|     "wordSequences": "Tu contraseña contiene secuencias", | ||||
|     "errorList": "Errores:", | ||||
|     "veryWeak": "Muy Débil", | ||||
|     "weak": "Débil", | ||||
|     "normal": "Normal", | ||||
|     "medium": "Media", | ||||
|     "strong": "Fuerte", | ||||
|     "veryStrong": "Muy Fuerte" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/fr.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/fr.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Votre mot de passe est trop court", | ||||
|     "wordMaxLength": "Votre mot de passe est trop long", | ||||
|     "wordInvalidChar": "Votre mot de passe contient un caractère invalide", | ||||
|     "wordNotEmail": "Ne pas utiliser votre adresse e-mail comme mot de passe", | ||||
|     "wordSimilarToUsername": "Votre mot de passe ne peut pas contenir votre nom d'utilisateur", | ||||
|     "wordTwoCharacterClasses": "Utilisez différents type de caractères", | ||||
|     "wordRepetitions": "Trop de répétitions", | ||||
|     "wordSequences": "Votre mot de passe contient des séquences", | ||||
|     "errorList": "Erreurs:", | ||||
|     "veryWeak": "Très Faible", | ||||
|     "weak": "Faible", | ||||
|     "normal": "Normal", | ||||
|     "medium": "Moyen", | ||||
|     "strong": "Fort", | ||||
|     "veryStrong": "Très Fort" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/it.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/it.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
| 	"wordMinLength": "La tua password è troppo corta", | ||||
| 	"wordMaxLength": "La tua password è troppo lunga", | ||||
| 	"wordInvalidChar": "La tua password contiene un carattere non valido", | ||||
| 	"wordNotEmail": "Non usare la tua e-mail come password", | ||||
| 	"wordSimilarToUsername": "La tua password non può contenere il tuo nome", | ||||
| 	"wordTwoCharacterClasses": "Usa classi di caratteri diversi", | ||||
| 	"wordRepetitions": "Troppe ripetizioni", | ||||
| 	"wordSequences": "La tua password contiene sequenze", | ||||
| 	"errorList": "Errori:", | ||||
| 	"veryWeak": "Molto debole", | ||||
| 	"weak": "Debole", | ||||
| 	"normal": "Normale", | ||||
| 	"medium": "Media", | ||||
| 	"strong": "Forte", | ||||
| 	"veryStrong": "Molto forte" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/no.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/no.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
| "wordMinLength": "Ditt passord er for kort", | ||||
| "wordMaxLength": "Ditt passord er for langt", | ||||
| "wordInvalidChar": "Ditt passord inneholder et ugyldig tegn", | ||||
| "wordNotEmail": "Ikke bruk din epost som ditt passord", | ||||
| "wordSimilarToUsername": "Ditt passord er for likt ditt brukernavn", | ||||
| "wordTwoCharacterClasses": "Bruk en kombinasjon av bokstaver, tall og andre tegn", | ||||
| "wordRepetitions": "For mange repitisjoner", | ||||
| "wordSequences": "Ditt passord inneholder repeterende tegn", | ||||
| "errorList": "Feil:", | ||||
| "veryWeak": "Veldig Svakt", | ||||
| "weak": "Svakt", | ||||
| "normal": "Normal", | ||||
| "medium": "Medium", | ||||
| "strong": "Sterkt", | ||||
| "veryStrong": "Veldig Sterkt" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/pl.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/pl.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Hasło jest zbyt krótkie", | ||||
|     "wordMaxLength": "Hasło jest za długie", | ||||
|     "wordInvalidChar": "Hasło zawiera nieprawidłowy znak", | ||||
|     "wordNotEmail": "Hasło nie może być Twoim emailem", | ||||
|     "wordSimilarToUsername": "Hasło nie może zawierać nazwy użytkownika", | ||||
|     "wordTwoCharacterClasses": "Użyj innych klas znaków", | ||||
|     "wordRepetitions": "Zbyt wiele powtórzeń", | ||||
|     "wordSequences": "Hasło zawiera sekwencje", | ||||
|     "errorList": "Błędy:", | ||||
|     "veryWeak": "Bardzo słabe", | ||||
|     "weak": "Słabe", | ||||
|     "normal": "Normalne", | ||||
|     "medium": "Średnie", | ||||
|     "strong": "Silne", | ||||
|     "veryStrong": "Bardzo silne" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/pt.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/pt.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Sua senha é muito curta", | ||||
|     "wordMaxLength": "Sua senha é muito longa", | ||||
|     "wordInvalidChar": "Sua senha contém um caractere inválido", | ||||
|     "wordNotEmail": "Não use seu e-mail como senha", | ||||
|     "wordSimilarToUsername": "Sua senha não pode conter o seu nome de usuário", | ||||
|     "wordTwoCharacterClasses": "Use diferentes classes de caracteres", | ||||
|     "wordRepetitions": "Muitas repetições", | ||||
|     "wordSequences": "Sua senha contém sequências", | ||||
|     "errorList": "Erros:", | ||||
|     "veryWeak": "Muito Fraca", | ||||
|     "weak": "Fraca", | ||||
|     "normal": "Normal", | ||||
|     "medium": "Média", | ||||
|     "strong": "Forte", | ||||
|     "veryStrong": "Muito Forte" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/ru.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/ru.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Слишком короткий пароль", | ||||
|     "wordMaxLength": "Ваш пароль слишком длинный", | ||||
|     "wordInvalidChar": "Ваш пароль содержит недопустимый символ", | ||||
|     "wordNotEmail": "Не используйте e-mail в качестве пароля", | ||||
|     "wordSimilarToUsername": "Пароль не должен содержать логин", | ||||
|     "wordTwoCharacterClasses": "Используйте разные классы символов", | ||||
|     "wordRepetitions": "Слишком много повторений", | ||||
|     "wordSequences": "Пароль содержит последовательности", | ||||
|     "errorList": "Ошибки:", | ||||
|     "veryWeak": "Очень слабый", | ||||
|     "weak": "Слабый", | ||||
|     "normal": "Нормальный", | ||||
|     "medium": "Средний", | ||||
|     "strong": "Сильный", | ||||
|     "veryStrong": "Очень сильный" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/sk.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/sk.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Vaše heslo je príliž krátke", | ||||
|     "wordMaxLength": "Vaše heslo je príliš dlhé", | ||||
|     "wordInvalidChar": "Vaše heslo obsahuje neplatný znak", | ||||
|     "wordNotEmail": "Nepoužívajte Váš email ako Vaše heslo", | ||||
|     "wordSimilarToUsername": "Vaše heslo nesmie obsahovať prihlasovacie meno", | ||||
|     "wordTwoCharacterClasses": "Použite rôzne druhy znakov", | ||||
|     "wordRepetitions": "Príliš veľa opakovaní", | ||||
|     "wordSequences": "Vaše heslo obsahuje postupnosť", | ||||
|     "errorList": "Chyby:", | ||||
|     "veryWeak": "Veľmi slabé", | ||||
|     "weak": "Slabé", | ||||
|     "normal": "Normálne", | ||||
|     "medium": "Stredne silné", | ||||
|     "strong": "Silné", | ||||
|     "veryStrong": "Veľmi silné" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/th.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/th.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "รหัสผ่านของคุณสั้นเกินไป", | ||||
|     "wordMaxLength": "รหัสผ่านของคุณยาวเกินไป", | ||||
|     "wordInvalidChar": "รหัสผ่านของคุณมีอักษรที่ไม่ถูกต้อง", | ||||
|     "wordNotEmail": "คุณไม่สามารถใช้รหัสผ่านเหมือนกับอีเมล์ของคุณได้", | ||||
|     "wordSimilarToUsername": "รหัสผ่านไม่ควรประกอบด้วยคำที่เป็น username", | ||||
|     "wordTwoCharacterClasses": "ลองเป็นกลุ่มคำใหม่", | ||||
|     "wordRepetitions": "มีอักษรซ้ำเยอะเกินไป", | ||||
|     "wordSequences": "รหัสผ่านของคุณเดาง่ายเกินไป", | ||||
|     "errorList": "Errors:", | ||||
|     "veryWeak": "เดาง่ายมาก", | ||||
|     "weak": "เดาง่าย", | ||||
|     "normal": "พอใช้", | ||||
|     "medium": "กำลังดี", | ||||
|     "strong": "ค่อนข้างดี", | ||||
|     "veryStrong": "ดีมาก" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/tr.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/tr.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "wordMinLength": "Girdiğiniz şifre çok Kısa", | ||||
|     "wordMaxLength": "Parolanız çok uzun", | ||||
|     "wordInvalidChar": "Şifreniz geçersiz bir karakter içeriyor", | ||||
|     "wordNotEmail": "E-mail adresinizi şifreniz içerisinde kullanmayınız", | ||||
|     "wordSimilarToUsername": "Kullanıcı Adınızı şifreniz içerisinde kullanmayınız", | ||||
|     "wordTwoCharacterClasses": "Başka karakter sınıfı kullanınız", | ||||
|     "wordRepetitions": "Çok fazla tekrar var", | ||||
|     "wordSequences": "Şifreniz Dizi içermektedir", | ||||
|     "errorList": "Hatalar:", | ||||
|     "veryWeak": "Çok Zayıf", | ||||
|     "weak": "Zayıf", | ||||
|     "normal": "Normal", | ||||
|     "medium": "Orta", | ||||
|     "strong": "Güçlü", | ||||
|     "veryStrong": "Çok Güçlü" | ||||
| } | ||||
							
								
								
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/zh-TW.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/js/libs/pwstrength/locales/zh-TW.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
| 	"wordMinLength": "您的密碼太短", | ||||
| 	"wordMaxLength": "您的密碼太長", | ||||
| 	"wordInvalidChar": "您的密碼包含無效字符", | ||||
| 	"wordNotEmail": "不要使用電子郵件作為密碼", | ||||
| 	"wordSimilarToUsername": "您的密碼不能包含您的用戶名", | ||||
| 	"wordTwoCharacterClasses": "使用不同的字元類型 例如: 大小寫混合", | ||||
| 	"wordRepetitions": "太多的重複。例如:1111", | ||||
| 	"wordSequences": "你的密碼包含連續英/數字 例如:123 or abc", | ||||
| 	"errorList": "錯誤:", | ||||
| 	"veryWeak": "非常弱", | ||||
| 	"weak": "弱", | ||||
| 	"normal": "普通", | ||||
| 	"medium": "中等", | ||||
| 	"strong": "強", | ||||
| 	"veryStrong": "非常強" | ||||
| } | ||||
							
								
								
									
										1223
									
								
								cps/static/js/libs/pwstrength/pwstrength-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1223
									
								
								cps/static/js/libs/pwstrength/pwstrength-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										4
									
								
								cps/static/js/libs/pwstrength/pwstrength-bootstrap.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								cps/static/js/libs/pwstrength/pwstrength-bootstrap.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										65
									
								
								cps/static/js/password.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cps/static/js/password.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| /* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||
|  *    Copyright (C) 2022  OzzieIsaacs | ||||
|  * | ||||
|  *  This program is free software: you can redistribute it and/or modify | ||||
|  *  it under the terms of the GNU General Public License as published by | ||||
|  *  the Free Software Foundation, either version 3 of the License, or | ||||
|  *  (at your option) any later version. | ||||
|  * | ||||
|  *  This program is distributed in the hope that it will be useful, | ||||
|  *  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  *  GNU General Public License for more details. | ||||
|  * | ||||
|  *  You should have received a copy of the GNU General Public License | ||||
|  *  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| $(document).ready(function() { | ||||
|     i18next.use(i18nextHttpBackend).init({ | ||||
|         lng: $('#password').data("lang"), | ||||
|         debug: false, | ||||
|         fallbackLng: 'en', | ||||
|           backend: { | ||||
|               loadPath: getPath() + "/static/js/libs/pwstrength/locales/{{lng}}.json", | ||||
|           }, | ||||
|  | ||||
|         }, function () { | ||||
|         if ($('#password').data("verify")) { | ||||
|             // Initialized and ready to go | ||||
|             var options = {}; | ||||
|             options.common = { | ||||
|                 minChar: $('#password').data("min"), | ||||
|                 maxChar: -1 | ||||
|             } | ||||
|             options.ui = { | ||||
|                 bootstrap3: true, | ||||
|                 showProgressBar: false, | ||||
|                 showErrors: true, | ||||
|                 showVerdicts: false, | ||||
|             } | ||||
|             options.rules= { | ||||
|                 specialCharClass: "(?=.*?[^A-Za-z\\s0-9])", | ||||
|                 activated: { | ||||
|                     wordNotEmail: false, | ||||
|                     wordMinLength: $('#password').data("min"), | ||||
|                     // wordMaxLength: false, | ||||
|                     // wordInvalidChar: true, | ||||
|                     wordSimilarToUsername: false, | ||||
|                     wordSequences: false, | ||||
|                     wordTwoCharacterClasses: false, | ||||
|                     wordRepetitions: false, | ||||
|                     wordLowercase: $('#password').data("lower") === "True" ? true : false, | ||||
|                     wordUppercase: $('#password').data("upper") === "True" ? true : false, | ||||
|                     wordOneNumber: $('#password').data("number") === "True" ? true : false, | ||||
|                     wordThreeNumbers: false, | ||||
|                     wordOneSpecialChar: $('#password').data("special") === "True" ? true : false, | ||||
|                     // wordTwoSpecialChar: true, | ||||
|                     wordUpperLowerCombo: false, | ||||
|                     wordLetterNumberCombo: false, | ||||
|                     wordLetterNumberCharCombo: false | ||||
|                 } | ||||
|             } | ||||
|             $('#password').pwstrength(options); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @@ -634,7 +634,7 @@ function UserActions (value, row) { | ||||
|  | ||||
| /* Function for cancelling tasks */ | ||||
| function TaskActions (value, row) { | ||||
|     var cancellableStats = [0, 1, 2]; | ||||
|     var cancellableStats = [0, 2]; | ||||
|     if (row.task_id && row.is_cancellable && cancellableStats.includes(row.stat)) { | ||||
|         return [ | ||||
|             "<div class=\"danger task-cancel\" data-toggle=\"modal\" data-target=\"#cancelTaskModal\" data-task-id=\"" + row.task_id + "\" title=\"Cancel\">", | ||||
|   | ||||
| @@ -202,8 +202,8 @@ class TaskEmail(CalibreTask): | ||||
|             self.asyncSMTP.set_debuglevel(1) | ||||
|         if use_ssl == 1: | ||||
|             self.asyncSMTP.starttls() | ||||
|         if self.settings["mail_password"]: | ||||
|             self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) | ||||
|         if self.settings["mail_password_e"]: | ||||
|             self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password_e"])) | ||||
|  | ||||
|         # Convert message to something to send | ||||
|         fp = StringIO() | ||||
|   | ||||
| @@ -22,8 +22,8 @@ from urllib.request import urlopen | ||||
| from lxml import etree | ||||
| from html import escape | ||||
|  | ||||
| from cps import config, db, fs, gdriveutils, logger, ub | ||||
| from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED | ||||
| from cps import config, db, gdriveutils, logger | ||||
| from cps.services.worker import CalibreTask | ||||
| from flask_babel import lazy_gettext as N_ | ||||
|  | ||||
| OPF_NAMESPACE = "http://www.idpf.org/2007/opf" | ||||
| @@ -74,7 +74,10 @@ class TaskBackupMetadata(CalibreTask): | ||||
|     def backup_metadata(self): | ||||
|         try: | ||||
|             metadata_backup = self.calibre_db.session.query(db.Metadata_Dirtied).all() | ||||
|             custom_columns = self.calibre_db.session.query(db.CustomColumns).order_by(db.CustomColumns.label).all() | ||||
|             custom_columns = (self.calibre_db.session.query(db.CustomColumns) | ||||
|                               .filter(db.CustomColumns.mark_for_delete == 0) | ||||
|                               .filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)) | ||||
|                               .order_by(db.CustomColumns.label).all()) | ||||
|             count = len(metadata_backup) | ||||
|             i = 0 | ||||
|             for backup in metadata_backup: | ||||
| @@ -226,11 +229,11 @@ class TaskBackupMetadata(CalibreTask): | ||||
|         # doc = etree.tostring(package, xml_declaration=True, encoding='utf-8', pretty_print=True) # .replace(b"&quot;", b""") | ||||
|         try: | ||||
|             with open(book_metadata_filepath, 'wb') as f: | ||||
|                 # f.write(doc) | ||||
|                 doc.write(f, xml_declaration=True, encoding='utf-8', pretty_print=True) | ||||
|         except Exception: | ||||
|             # ToDo: Folder not writeable error | ||||
|             pass | ||||
|  | ||||
|     @property | ||||
|     def name(self): | ||||
|         return "Metadata backup" | ||||
|   | ||||
| @@ -61,27 +61,27 @@ | ||||
|     <div class="col"> | ||||
|       <h2>{{_('Email Server Settings')}}</h2> | ||||
|       {% if config.get_mail_server_configured() %} | ||||
|         {% if email.mail_server_type == 0 %} | ||||
|         {% if config.mail_server_type == 0 %} | ||||
|         <div class="col-xs-12 col-sm-12"> | ||||
|           <div class="row"> | ||||
|             <div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{email.mail_server}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{config.mail_server}}</div> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{email.mail_port}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{config.mail_port}}</div> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{ display_bool_setting(email.mail_use_ssl) }}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.mail_use_ssl) }}</div> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{email.mail_login}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{config.mail_login}}</div> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col-xs-6 col-sm-3">{{_('From Email')}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{email.mail_from}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{config.mail_from}}</div> | ||||
|           </div> | ||||
|         </div> | ||||
|       {% else %} | ||||
| @@ -92,7 +92,7 @@ | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col-xs-6 col-sm-3">{{_('From Email')}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{email.mail_gmail_token['email']}}</div> | ||||
|             <div class="col-xs-6 col-sm-3">{{config.mail_gmail_token['email']}}</div> | ||||
|           </div> | ||||
|         </div> | ||||
|       {% endif %} | ||||
|   | ||||
| @@ -159,8 +159,8 @@ | ||||
|         <input type="text" class="form-control" id="config_goodreads_api_key" name="config_goodreads_api_key" value="{% if config.config_goodreads_api_key != None %}{{ config.config_goodreads_api_key }}{% endif %}" autocomplete="off"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label for="config_goodreads_api_secret">{{_('Goodreads API Secret')}}</label> | ||||
|         <input type="text" class="form-control" id="config_goodreads_api_secret" name="config_goodreads_api_secret" value="{% if config.config_goodreads_api_secret != None %}{{ config.config_goodreads_api_secret }}{% endif %}" autocomplete="off"> | ||||
|         <label for="config_goodreads_api_secret_e">{{_('Goodreads API Secret')}}</label> | ||||
|         <input type="password" class="form-control" id="config_goodreads_api_secret_e" name="config_goodreads_api_secret_e" value="{% if config.config_goodreads_api_secret_e != None %}{{ config.config_goodreads_api_secret_e }}{% endif %}" autocomplete="off"> | ||||
|       </div> | ||||
|     </div> | ||||
|     {% endif %} | ||||
| @@ -245,8 +245,8 @@ | ||||
|         </div> | ||||
|         <div data-related="ldap-auth-password-2"> | ||||
|           <div class="form-group"> | ||||
|             <label for="config_ldap_serv_password">{{_('LDAP Administrator Password')}}</label> | ||||
|             <input type="password" class="form-control" id="config_ldap_serv_password" name="config_ldap_serv_password" value="" autocomplete="off"> | ||||
|             <label for="config_ldap_serv_password_e">{{_('LDAP Administrator Password')}}</label> | ||||
|             <input type="password" class="form-control" id="config_ldap_serv_password_e" name="config_ldap_serv_password_e" value="" autocomplete="off"> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
| @@ -353,6 +353,57 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="panel panel-default"> | ||||
|     <div class="panel-heading"> | ||||
|       <h4 class="panel-title"> | ||||
|         <a class="accordion-toggle" data-toggle="collapse" href="#collapsesix"> | ||||
|           <span class="glyphicon glyphicon-plus"></span> | ||||
|            {{_('Securitiy Settings')}} | ||||
|         </a> | ||||
|       </h4> | ||||
|     </div> | ||||
|     <div id="collapsesix" class="panel-collapse collapse"> | ||||
|       <div class="panel-body"> | ||||
|             <div class="form-group"> | ||||
|               <input type="checkbox" id="config_ratelimiter" name="config_ratelimiter" {% if config.config_ratelimiter %}checked{% endif %}> | ||||
|               <label for="config_ratelimiter">{{_('Limit failed login attempts')}}</label> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|               <label for="config_session">{{_('Session protection')}}</label> | ||||
|                 <select name="config_session" id="config_session" class="form-control"> | ||||
|                   <option value="0" {% if config.config_session == 0 %}selected{% endif %}>{{_('Basic')}}</option> | ||||
|                   <option value="1" {% if config.config_session == 1 %}selected{% endif %}>{{_('Strong')}}</option> | ||||
|                 </select> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <input type="checkbox" id="config_password_policy" data-control="password_settings" name="config_password_policy" {% if config.config_password_policy %}checked{% endif %}> | ||||
|                 <label for="config_password_policy">{{_('User Password policy')}}</label> | ||||
|             </div> | ||||
|             <div data-related="password_settings"> | ||||
|               <div class="form-group" style="margin-left:10px;"> | ||||
|                 <label for="config_password_min_length">{{_('Minimum password length')}}</label> | ||||
|                 <input type="number" min="1" max="40" class="form-control" name="config_password_min_length" id="config_password_min_length" value="{% if config.config_password_min_length != None %}{{ config.config_password_min_length }}{% endif %}" autocomplete="off" required> | ||||
|               </div> | ||||
|               <div class="form-group" style="margin-left:10px;"> | ||||
|                 <input type="checkbox" id="config_password_number" name="config_password_number"  {% if config.config_password_number %}checked{% endif %}> | ||||
|                 <label for="config_password_number">{{_('Enforce number')}}</label> | ||||
|               </div> | ||||
|               <div class="form-group" style="margin-left:10px;"> | ||||
|                 <input type="checkbox" id="config_password_lower" name="config_password_lower"  {% if config.config_password_lower %}checked{% endif %}> | ||||
|                 <label for="config_password_lower">{{_('Enforce lowercase characters')}}</label> | ||||
|               </div> | ||||
|               <div class="form-group" style="margin-left:10px;"> | ||||
|                 <input type="checkbox" id="config_password_upper" name="config_password_upper"  {% if config.config_password_upper %}checked{% endif %}> | ||||
|                 <label for="config_password_upper">{{_('Enforce uppercase characters')}}</label> | ||||
|               </div> | ||||
|               <div class="form-group" style="margin-left:10px;"> | ||||
|                 <input type="checkbox" id="config_password_special" name="config_password_special"  {% if config.config_password_special %}checked{% endif %}> | ||||
|                 <label for="config_password_special">{{_('Enforce special characters')}}</label> | ||||
|               </div> | ||||
|             </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|     <div class="col-sm-12"> | ||||
|     <button type="button" name="submit" id="config_submit" class="btn btn-default">{{_('Save')}}</button> | ||||
|   | ||||
| @@ -48,8 +48,8 @@ | ||||
|         <input type="text" class="form-control" name="mail_login" id="mail_login" value="{{content.mail_login}}"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label for="mail_password">{{_('SMTP Password')}}</label> | ||||
|         <input type="password" class="form-control" name="mail_password" id="mail_password" value="{{content.mail_password}}"> | ||||
|         <label for="mail_password_e">{{_('SMTP Password')}}</label> | ||||
|         <input type="password" class="form-control" name="mail_password_e" id="mail_password_e" value="{{content.mail_password_e}}"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label for="mail_from">{{_('From Email')}}</label> | ||||
|   | ||||
							
								
								
									
										10
									
								
								cps/templates/locales/en/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cps/templates/locales/en/translation.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| { | ||||
|   "input": { | ||||
|     "placeholder": "a placeholder" | ||||
|   }, | ||||
|   "nav": { | ||||
|     "home": "Home", | ||||
|     "page1": "Page One", | ||||
|     "page2": "Page Two" | ||||
|   } | ||||
| } | ||||
| @@ -7,11 +7,11 @@ | ||||
|     <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||
|     <div class="form-group"> | ||||
|       <label for="username">{{_('Username')}}</label> | ||||
|       <input type="text" class="form-control" id="username" name="username" autocapitalize="off" placeholder="{{_('Username')}}"> | ||||
|       <input type="text" class="form-control" id="username" name="username" autocapitalize="off" placeholder="{{_('Username')}}" value="{{ username }}"> | ||||
|     </div> | ||||
|     <div class="form-group"> | ||||
|       <label for="password">{{_('Password')}}</label> | ||||
|       <input type="password" class="form-control" id="password" name="password" placeholder="{{_('Password')}}"> | ||||
|       <input type="password" class="form-control" id="password" name="password" placeholder="{{_('Password')}}" value="{{ password }}"> | ||||
|     </div> | ||||
|     <div class="checkbox"> | ||||
|       <label> | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|       {% endif %} | ||||
|         <div class="form-group"> | ||||
|           <label for="password">{{_('Password')}}</label> | ||||
|           <input type="password" class="form-control" name="password" id="password" value="" autocomplete="off"> | ||||
|           <input type="password" class="form-control" name="password" id="password" data-lang="{{ current_user.locale }}" data-verify="{{ config.config_password_policy }}" {% if config.config_password_policy %} data-min={{ config.config_password_min_length }} data-special={{ config.config_password_special }} data-upper={{ config.config_password_upper }} data-lower={{ config.config_password_lower }} data-number={{ config.config_password_number }}{% endif %} value="" autocomplete="off"> | ||||
|         </div> | ||||
|     {% endif %} | ||||
|     <div class="form-group"> | ||||
| @@ -175,5 +175,9 @@ | ||||
| <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.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> | ||||
| <script src="{{ url_for('static', filename='js/libs/pwstrength/i18next.min.js') }}"></script> | ||||
| <script src="{{ url_for('static', filename='js/libs/pwstrength/i18nextHttpBackend.min.js') }}"></script> | ||||
| <script src="{{ url_for('static', filename='js/libs/pwstrength/pwstrength-bootstrap.min.js') }}"></script> | ||||
| <script src="{{ url_for('static', filename='js/password.js') }}"></script> | ||||
| <script src="{{ url_for('static', filename='js/table.js') }}"></script> | ||||
| {% endblock %} | ||||
|   | ||||
							
								
								
									
										11
									
								
								cps/ub.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								cps/ub.py
									
									
									
									
									
								
							| @@ -55,6 +55,7 @@ from werkzeug.security import generate_password_hash | ||||
|  | ||||
| from . import constants, logger | ||||
|  | ||||
|  | ||||
| log = logger.create() | ||||
|  | ||||
| session = None | ||||
| @@ -817,7 +818,7 @@ def init_db_thread(): | ||||
|     return Session() | ||||
|  | ||||
|  | ||||
| def init_db(app_db_path, user_credentials=None): | ||||
| def init_db(app_db_path): | ||||
|     # Open session for database connection | ||||
|     global session | ||||
|     global app_DB_path | ||||
| @@ -838,6 +839,7 @@ def init_db(app_db_path, user_credentials=None): | ||||
|         create_admin_user(session) | ||||
|         create_anonymous_user(session) | ||||
|  | ||||
| def password_change(user_credentials=None): | ||||
|     if user_credentials: | ||||
|         username, password = user_credentials.split(':', 1) | ||||
|         user = session.query(User).filter(func.lower(User.name) == username.lower()).first() | ||||
| @@ -845,7 +847,12 @@ def init_db(app_db_path, user_credentials=None): | ||||
|             if not password: | ||||
|                 print("Empty password is not allowed") | ||||
|                 sys.exit(4) | ||||
|             user.password = generate_password_hash(password) | ||||
|             try: | ||||
|                 from .helper import valid_password | ||||
|                 user.password = generate_password_hash(valid_password(password)) | ||||
|             except Exception: | ||||
|                 print("Password doesn't comply with password validation rules") | ||||
|                 sys.exit(4) | ||||
|             if session_commit() == "": | ||||
|                 print("Password for user '{}' changed".format(username)) | ||||
|                 sys.exit(0) | ||||
|   | ||||
| @@ -401,7 +401,7 @@ class Updater(threading.Thread): | ||||
|             os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db', | ||||
|             os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json', | ||||
|             os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv', | ||||
|             os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2', | ||||
|             os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2', os.sep + '.key', | ||||
|             os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so', os.sep + 'cps' + os.sep + '.HOMEDIR', | ||||
|             os.sep + 'gmail.json', os.sep + 'exclude.txt', os.sep + 'cps' + os.sep + 'cache' | ||||
|         ] | ||||
|   | ||||
| @@ -23,7 +23,7 @@ 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, logger | ||||
| from . import lm, ub, config, constants, services, logger, limiter | ||||
|  | ||||
| log = logger.create() | ||||
|  | ||||
| @@ -52,6 +52,7 @@ def requires_basic_auth_if_no_ano(f): | ||||
|             login_result, error = services.ldap.bind_user(auth.username, auth.password) | ||||
|             if login_result: | ||||
|                 user = _fetch_user_by_name(auth.username) | ||||
|                 [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] | ||||
|                 login_user(user) | ||||
|                 return f(*args, **kwargs) | ||||
|             elif login_result is not None: | ||||
| @@ -65,8 +66,10 @@ def requires_basic_auth_if_no_ano(f): | ||||
|  | ||||
|  | ||||
| def _load_user_from_auth_header(username, password): | ||||
|     limiter.check() | ||||
|     user = _fetch_user_by_name(username) | ||||
|     if bool(user and check_password_hash(str(user.password), password)): | ||||
|     if bool(user and check_password_hash(str(user.password), password)) and user.name != "Guest": | ||||
|         [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] | ||||
|         login_user(user) | ||||
|         return user | ||||
|     else: | ||||
| @@ -101,6 +104,7 @@ def load_user_from_reverse_proxy_header(req): | ||||
|             if rp_header_username: | ||||
|                 user = _fetch_user_by_name(rp_header_username) | ||||
|                 if user: | ||||
|                     [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] | ||||
|                     login_user(user) | ||||
|                     return user | ||||
|     return None | ||||
|   | ||||
							
								
								
									
										191
									
								
								cps/web.py
									
									
									
									
									
								
							
							
						
						
									
										191
									
								
								cps/web.py
									
									
									
									
									
								
							| @@ -30,6 +30,8 @@ from flask import session as flask_session | ||||
| from flask_babel import gettext as _ | ||||
| from flask_babel import get_locale | ||||
| from flask_login import login_user, logout_user, login_required, current_user | ||||
| from flask_limiter import RateLimitExceeded | ||||
| from flask_limiter.util import get_remote_address | ||||
| 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 | ||||
| @@ -46,7 +48,7 @@ from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download | ||||
| from .helper import check_valid_domain, check_email, check_username, \ | ||||
|     get_book_cover, get_series_cover_thumbnail, get_download_link, send_mail, generate_random_password, \ | ||||
|     send_registration_mail, check_send_to_ereader, check_read_formats, tags_filters, reset_password, valid_email, \ | ||||
|     edit_book_read_status | ||||
|     edit_book_read_status, valid_password | ||||
| from .pagination import Pagination | ||||
| from .redirect import redirect_back | ||||
| from .babel import get_available_locale | ||||
| @@ -54,9 +56,11 @@ from .usermanagement import login_required_if_no_ano | ||||
| 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 .services.worker import WorkerThread | ||||
| from .tasks_status import render_task_status | ||||
|  | ||||
|  | ||||
| feature_support = { | ||||
|     'ldap': bool(services.ldap), | ||||
|     'goodreads': bool(services.goodreads_support), | ||||
| @@ -1230,19 +1234,23 @@ def send_to_ereader(book_id, book_format, convert): | ||||
|  | ||||
| # ################################### Login Logout ################################################################## | ||||
|  | ||||
|  | ||||
| @web.route('/register', methods=['GET', 'POST']) | ||||
| def register(): | ||||
| @web.route('/register', methods=['POST']) | ||||
| @limiter.limit("40/day", key_func=get_remote_address) | ||||
| @limiter.limit("3/minute", key_func=get_remote_address) | ||||
| def register_post(): | ||||
|     if not config.config_public_reg: | ||||
|         abort(404) | ||||
|     to_save = request.form.to_dict() | ||||
|     try: | ||||
|         limiter.check() | ||||
|     except RateLimitExceeded: | ||||
|         flash(_(u"Please wait one minute to register next user"), category="error") | ||||
|         return render_title_template('register.html', config=config, title=_("Register"), page="register") | ||||
|     if current_user is not None and current_user.is_authenticated: | ||||
|         return redirect(url_for('web.index')) | ||||
|     if not config.get_mail_server_configured(): | ||||
|         flash(_("Oops! Email server is not configured, please contact your administrator."), category="error") | ||||
|         return render_title_template('register.html', title=_("Register"), page="register") | ||||
|  | ||||
|     if request.method == "POST": | ||||
|         to_save = request.form.to_dict() | ||||
|     nickname = to_save.get("email", "").strip() if config.config_register_email else to_save.get('name') | ||||
|     if not nickname or not to_save.get("email"): | ||||
|         flash(_("Oops! Please complete all fields."), category="error") | ||||
| @@ -1258,7 +1266,7 @@ def register(): | ||||
|     if check_valid_domain(email): | ||||
|         content.name = nickname | ||||
|         content.email = email | ||||
|             password = generate_random_password() | ||||
|         password = generate_random_password(config.config_password_min_length) | ||||
|         content.password = generate_password_hash(password) | ||||
|         content.role = config.config_default_role | ||||
|         content.locale = config.config_default_locale | ||||
| @@ -1275,79 +1283,35 @@ def register(): | ||||
|             return render_title_template('register.html', title=_("Register"), page="register") | ||||
|     else: | ||||
|         flash(_("Oops! Your Email is not allowed."), category="error") | ||||
|             log.warning('Registering failed for user "{}" Email: {}'.format(nickname, | ||||
|                                                                                      to_save.get("email",""))) | ||||
|         log.warning('Registering failed for user "{}" Email: {}'.format(nickname, to_save.get("email",""))) | ||||
|         return render_title_template('register.html', title=_("Register"), page="register") | ||||
|     flash(_("Success! Confirmation Email has been sent."), category="success") | ||||
|     return redirect(url_for('web.login')) | ||||
|  | ||||
|  | ||||
| @web.route('/register', methods=['GET']) | ||||
| def register(): | ||||
|     if not config.config_public_reg: | ||||
|         abort(404) | ||||
|     if current_user is not None and current_user.is_authenticated: | ||||
|         return redirect(url_for('web.index')) | ||||
|     if not config.get_mail_server_configured(): | ||||
|         flash(_("Oops! Email server is not configured, please contact your administrator."), category="error") | ||||
|         return render_title_template('register.html', title=_("Register"), page="register") | ||||
|     if feature_support['oauth']: | ||||
|         register_user_with_oauth() | ||||
|     return render_title_template('register.html', config=config, title=_("Register"), page="register") | ||||
|  | ||||
|  | ||||
| @web.route('/login', methods=['GET', 'POST']) | ||||
| def login(): | ||||
|     if current_user is not None and current_user.is_authenticated: | ||||
|         return redirect(url_for('web.index')) | ||||
|     if config.config_login_type == constants.LOGIN_LDAP and not services.ldap: | ||||
|         log.error("Cannot activate LDAP authentication") | ||||
|         flash(_("Oops! Cannot activate LDAP authentication"), category="error") | ||||
|     if request.method == "POST": | ||||
|         form = request.form.to_dict() | ||||
|         user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form['username'].strip().lower()) \ | ||||
|             .first() | ||||
|         if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "": | ||||
|             login_result, error = services.ldap.bind_user(form['username'], form['password']) | ||||
|             if login_result: | ||||
|                 login_user(user, remember=bool(form.get('remember_me'))) | ||||
| def handle_login_user(user, remember, message, category): | ||||
|     login_user(user, remember=remember) | ||||
|     ub.store_user_session() | ||||
|                 log.debug("You are now logged in as: '{}'".format(user.name)) | ||||
|                 flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), | ||||
|                       category="success") | ||||
|     flash(message, category=category) | ||||
|     [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] | ||||
|     return redirect_back(url_for("web.index")) | ||||
|             elif login_result is None and user and check_password_hash(str(user.password), form['password']) \ | ||||
|                     and user.name != "Guest": | ||||
|                 login_user(user, remember=bool(form.get('remember_me'))) | ||||
|                 ub.store_user_session() | ||||
|                 log.info("Local Fallback Login as: '{}'".format(user.name)) | ||||
|                 flash(_("Fallback Login as: %(nickname)s, LDAP Server not reachable, or user not known", | ||||
|                         nickname=user.name), | ||||
|                       category="warning") | ||||
|                 return redirect_back(url_for("web.index")) | ||||
|             elif login_result is None: | ||||
|                 log.info(error) | ||||
|                 flash(_("Oops! Login Failed: %(message)s", message=error), category="error") | ||||
|             else: | ||||
|                 ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) | ||||
|                 log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_address) | ||||
|                 flash(_("Oops! Invalid Username or Password."), category="error") | ||||
|         else: | ||||
|             ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) | ||||
|             if form.get('forgot', "") == 'forgot': | ||||
|                 if user is not None and user.name != "Guest": | ||||
|                     ret, __ = reset_password(user.id) | ||||
|                     if ret == 1: | ||||
|                         flash(_("Success! New Password was sent to your Email."), category="info") | ||||
|                         log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_address) | ||||
|                     else: | ||||
|                         log.error("An unknown error occurred. Please try again later") | ||||
|                         flash(_("Oops! An unknown error occurred. Please try again later."), category="error") | ||||
|                 else: | ||||
|                     flash(_("Oops! Please enter a valid username to reset password"), category="error") | ||||
|                     log.warning('Username missing for password reset IP-address: %s', ip_address) | ||||
|             else: | ||||
|                 if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest": | ||||
|                     login_user(user, remember=bool(form.get('remember_me'))) | ||||
|                     ub.store_user_session() | ||||
|                     log.debug("You are now logged in as: '%s'", user.name) | ||||
|                     flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), category="success") | ||||
|                     config.config_is_initial = False | ||||
|                     return redirect_back(url_for("web.index")) | ||||
|                 else: | ||||
|                     log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address)) | ||||
|                     flash(_("Oops! Invalid Username or Password."), category="error") | ||||
|  | ||||
|  | ||||
| def render_login(username="", password=""): | ||||
|     next_url = request.args.get('next', default=url_for("web.index"), type=str) | ||||
|     if url_for("web.logout") == next_url: | ||||
|         next_url = url_for("web.index") | ||||
| @@ -1355,10 +1319,91 @@ def login(): | ||||
|                                  title=_("Login"), | ||||
|                                  next_url=next_url, | ||||
|                                  config=config, | ||||
|                                  username=username, | ||||
|                                  password=password, | ||||
|                                  oauth_check=oauth_check, | ||||
|                                  mail=config.get_mail_server_configured(), page="login") | ||||
|  | ||||
|  | ||||
| @web.route('/login', methods=['GET']) | ||||
| def login(): | ||||
|     if current_user is not None and current_user.is_authenticated: | ||||
|         return redirect(url_for('web.index')) | ||||
|     if config.config_login_type == constants.LOGIN_LDAP and not services.ldap: | ||||
|         log.error(u"Cannot activate LDAP authentication") | ||||
|         flash(_(u"Cannot activate LDAP authentication"), category="error") | ||||
|     return render_login() | ||||
|  | ||||
|  | ||||
| @web.route('/login', methods=['POST']) | ||||
| @limiter.limit("40/day", key_func=lambda: request.form.get('username', "").strip().lower()) | ||||
| @limiter.limit("3/minute", key_func=lambda: request.form.get('username', "").strip().lower()) | ||||
| def login_post(): | ||||
|     form = request.form.to_dict() | ||||
|     try: | ||||
|         limiter.check() | ||||
|     except RateLimitExceeded: | ||||
|         flash(_(u"Please wait one minute before next login"), category="error") | ||||
|         return render_login(form.get("username", ""), form.get("password", "")) | ||||
|     if current_user is not None and current_user.is_authenticated: | ||||
|         return redirect(url_for('web.index')) | ||||
|     if config.config_login_type == constants.LOGIN_LDAP and not services.ldap: | ||||
|         log.error(u"Cannot activate LDAP authentication") | ||||
|         flash(_(u"Cannot activate LDAP authentication"), category="error") | ||||
|     user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form.get('username', "").strip().lower()) \ | ||||
|         .first() | ||||
|     remember_me = bool(form.get('remember_me')) | ||||
|     if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "": | ||||
|         login_result, error = services.ldap.bind_user(form['username'], form['password']) | ||||
|         if login_result: | ||||
|             log.debug(u"You are now logged in as: '{}'".format(user.name)) | ||||
|             return handle_login_user(user, | ||||
|                                      remember_me, | ||||
|                                      _(u"you are now logged in as: '%(nickname)s'", nickname=user.name), | ||||
|                                      "success") | ||||
|         elif login_result is None and user and check_password_hash(str(user.password), form['password']) \ | ||||
|                 and user.name != "Guest": | ||||
|             log.info("Local Fallback Login as: '{}'".format(user.name)) | ||||
|             return handle_login_user(user, | ||||
|                                      remember_me, | ||||
|                                      _(u"Fallback Login as: '%(nickname)s', " | ||||
|                                        u"LDAP Server not reachable, or user not known", nickname=user.name), | ||||
|                                      "warning") | ||||
|         elif login_result is None: | ||||
|             log.info(error) | ||||
|             flash(_(u"Could not login: %(message)s", message=error), category="error") | ||||
|         else: | ||||
|             ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) | ||||
|             log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_address) | ||||
|             flash(_(u"Wrong Username or Password"), category="error") | ||||
|     else: | ||||
|         ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) | ||||
|         if form.get('forgot', "") == 'forgot': | ||||
|             if user is not None and user.name != "Guest": | ||||
|                 ret, __ = reset_password(user.id) | ||||
|                 if ret == 1: | ||||
|                     flash(_(u"New Password was send to your email address"), category="info") | ||||
|                     log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_address) | ||||
|                 else: | ||||
|                     log.error(u"An unknown error occurred. Please try again later") | ||||
|                     flash(_(u"An unknown error occurred. Please try again later."), category="error") | ||||
|             else: | ||||
|                 flash(_(u"Please enter valid username to reset password"), category="error") | ||||
|                 log.warning('Username missing for password reset IP-address: %s', ip_address) | ||||
|         else: | ||||
|             if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest": | ||||
|                 config.config_is_initial = False | ||||
|                 log.debug(u"You are now logged in as: '{}'".format(user.name)) | ||||
|                 return handle_login_user(user, | ||||
|                                          remember_me, | ||||
|                                          _(u"You are now logged in as: '%(nickname)s'", nickname=user.name), | ||||
|                                          "success") | ||||
|             else: | ||||
|                 log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address)) | ||||
|                 flash(_(u"Wrong Username or Password"), category="error") | ||||
|     return render_login(form.get("username", ""), form.get("password", "")) | ||||
|  | ||||
|  | ||||
| @web.route('/logout') | ||||
| @login_required | ||||
| def logout(): | ||||
| @@ -1375,10 +1420,10 @@ def logout(): | ||||
| def change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages): | ||||
|     to_save = request.form.to_dict() | ||||
|     current_user.random_books = 0 | ||||
|     if current_user.role_passwd() or current_user.role_admin(): | ||||
|         if to_save.get("password"): | ||||
|             current_user.password = generate_password_hash(to_save.get("password")) | ||||
|     try: | ||||
|         if current_user.role_passwd() or current_user.role_admin(): | ||||
|             if to_save.get("password", "") != "": | ||||
| 	            current_user.password = generate_password_hash(to_save.get("password")) | ||||
|         if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail: | ||||
|             current_user.kindle_mail = valid_email(to_save.get("kindle_mail")) | ||||
|         new_email = valid_email(to_save.get("email", current_user.email)) | ||||
| @@ -1405,6 +1450,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations, | ||||
|         flash(str(ex), category="error") | ||||
|         return render_title_template("user_edit.html", | ||||
|                                      content=current_user, | ||||
|                                      config=config, | ||||
|                                      translations=translations, | ||||
|                                      profile=1, | ||||
|                                      languages=languages, | ||||
| @@ -1456,6 +1502,7 @@ def profile(): | ||||
|                                  profile=1, | ||||
|                                  languages=languages, | ||||
|                                  content=current_user, | ||||
|                                  config=config, | ||||
|                                  kobo_support=kobo_support, | ||||
|                                  title=_("%(name)s's Profile", name=current_user.name), | ||||
|                                  page="me", | ||||
|   | ||||
| @@ -18,3 +18,4 @@ lxml>=3.8.0,<5.0.0 | ||||
| flask-wtf>=0.14.2,<1.2.0 | ||||
| chardet>=3.0.0,<4.1.0 | ||||
| advocate>=1.0.0,<1.1.0 | ||||
| Flask-Limiter>=2.3.0,<3.3.0 | ||||
|   | ||||
| @@ -58,6 +58,7 @@ install_requires = | ||||
| 	flask-wtf>=0.14.2,<1.2.0 | ||||
| 	chardet>=3.0.0,<4.1.0 | ||||
| 	advocate>=1.0.0,<1.1.0 | ||||
| 	Flask-Limiter>=2.3.0,<3.2.0 | ||||
| 	 | ||||
|  | ||||
| [options.extras_require] | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Ozzie Isaacs
					Ozzie Isaacs