diff --git a/src/api/admin.rs b/src/api/admin.rs index 1d6f5f72..857850ad 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -1,8 +1,9 @@ use once_cell::sync::Lazy; use serde::de::DeserializeOwned; use serde_json::Value; -use std::process::Command; +use std::{env, process::Command, time::Duration}; +use reqwest::{blocking::Client, header::USER_AGENT}; use rocket::{ http::{Cookie, Cookies, SameSite}, request::{self, FlashMessage, Form, FromRequest, Outcome, Request}, @@ -18,7 +19,7 @@ use crate::{ db::{backup_database, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, - util::{get_display_size, format_naive_datetime_local}, + util::{format_naive_datetime_local, get_display_size}, CONFIG, }; @@ -47,9 +48,20 @@ pub fn routes() -> Vec { users_overview, organizations_overview, diagnostics, + get_diagnostics_config ] } +static DB_TYPE: Lazy<&str> = Lazy::new(|| { + DbConnType::from_url(&CONFIG.database_url()) + .map(|t| match t { + DbConnType::sqlite => "SQLite", + DbConnType::mysql => "MySQL", + DbConnType::postgresql => "PostgreSQL", + }) + .unwrap_or("Unknown") +}); + static CAN_BACKUP: Lazy = Lazy::new(|| { DbConnType::from_url(&CONFIG.database_url()) .map(|t| t == DbConnType::sqlite) @@ -307,7 +319,8 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { None => json!("Never") }; usr - }).collect(); + }) + .collect(); let text = AdminTemplateData::users(users_json).render()?; Ok(Html(text)) @@ -362,14 +375,16 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { #[get("/organizations/overview")] fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { let organizations = Organization::get_all(&conn); - let organizations_json: Vec = organizations.iter().map(|o| { + let organizations_json: Vec = organizations.iter() + .map(|o| { let mut org = o.to_json(); org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn)); org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn)); org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn)); org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32)); org - }).collect(); + }) + .collect(); let text = AdminTemplateData::organizations(organizations_json).render()?; Ok(Html(text)) @@ -391,77 +406,104 @@ struct GitCommit { } fn get_github_api(url: &str) -> Result { - use reqwest::{blocking::Client, header::USER_AGENT}; - use std::time::Duration; let github_api = Client::builder().build()?; - Ok( - github_api.get(url) + Ok(github_api + .get(url) .timeout(Duration::from_secs(10)) .header(USER_AGENT, "Bitwarden_RS") .send()? .error_for_status()? - .json::()? - ) + .json::()?) +} + +fn has_http_access() -> bool { + let http_access = Client::builder().build().unwrap(); + + match http_access + .head("https://github.com/dani-garcia/bitwarden_rs") + .timeout(Duration::from_secs(10)) + .header(USER_AGENT, "Bitwarden_RS") + .send() + { + Ok(r) => r.status().is_success(), + _ => false, + } } #[get("/diagnostics")] fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult> { - use std::net::ToSocketAddrs; - use chrono::prelude::*; use crate::util::read_file_string; + use chrono::prelude::*; + use std::net::ToSocketAddrs; + // Get current running versions let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json"); let vault_version_str = read_file_string(&vault_version_path)?; let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?; - let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next()); - let (dns_resolved, dns_ok) = match github_ips { - Ok(Some(a)) => (a.ip().to_string(), true), - _ => ("Could not resolve domain name.".to_string(), false), + // Execute some environment checks + let running_within_docker = std::path::Path::new("/.dockerenv").exists(); + let has_http_access = has_http_access(); + let uses_proxy = env::var_os("HTTP_PROXY").is_some() + || env::var_os("http_proxy").is_some() + || env::var_os("HTTPS_PROXY").is_some() + || env::var_os("https_proxy").is_some(); + + // Check if we are able to resolve DNS entries + let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { + Ok(Some(a)) => a.ip().to_string(), + _ => "Could not resolve domain name.".to_string(), }; - // If the DNS Check failed, do not even attempt to check for new versions since we were not able to resolve github.com - let (latest_release, latest_commit, latest_web_build) = if dns_ok { + // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. + // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already. + let (latest_release, latest_commit, latest_web_build) = if has_http_access { ( match get_github_api::("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") { Ok(r) => r.tag_name, - _ => "-".to_string() + _ => "-".to_string(), }, match get_github_api::("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") { Ok(mut c) => { c.sha.truncate(8); c.sha - }, - _ => "-".to_string() + } + _ => "-".to_string(), }, match get_github_api::("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") { Ok(r) => r.tag_name.trim_start_matches('v').to_string(), - _ => "-".to_string() + _ => "-".to_string(), }, ) } else { ("-".to_string(), "-".to_string(), "-".to_string()) }; - // Run the date check as the last item right before filling the json. - // This should ensure that the time difference between the browser and the server is as minimal as possible. - let dt = Utc::now(); - let server_time = dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); - let diagnostics_json = json!({ "dns_resolved": dns_resolved, - "server_time": server_time, "web_vault_version": web_vault_version.version, "latest_release": latest_release, "latest_commit": latest_commit, "latest_web_build": latest_web_build, + "running_within_docker": running_within_docker, + "has_http_access": has_http_access, + "uses_proxy": uses_proxy, + "db_type": *DB_TYPE, + "admin_url": format!("{}/diagnostics", admin_url(Referer(None))), + "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference }); let text = AdminTemplateData::diagnostics(diagnostics_json).render()?; Ok(Html(text)) } +#[get("/diagnostics/config")] +fn get_diagnostics_config(_token: AdminToken) -> JsonResult { + let support_json = CONFIG.get_support_json(); + Ok(Json(support_json)) +} + #[post("/config", data = "")] fn post_config(data: Json, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index a925468b..a2af79a5 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -172,7 +172,7 @@ fn hibp_breach(username: String) -> JsonResult { "Domain": "haveibeenpwned.com", "BreachDate": "2019-08-18T00:00:00Z", "AddedDate": "2019-08-18T00:00:00Z", - "Description": format!("Go to: https://haveibeenpwned.com/account/{account} for a manual check.

HaveIBeenPwned API key not set!
Go to https://haveibeenpwned.com/API/Key to purchase an API key from HaveIBeenPwned.

", account=username), + "Description": format!("Go to: https://haveibeenpwned.com/account/{account} for a manual check.

HaveIBeenPwned API key not set!
Go to https://haveibeenpwned.com/API/Key to purchase an API key from HaveIBeenPwned.

", account=username), "LogoPath": "bwrs_static/hibp.png", "PwnCount": 0, "DataClasses": [ diff --git a/src/config.rs b/src/config.rs index b5047cec..1422ddf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use std::process::exit; use std::sync::RwLock; use once_cell::sync::Lazy; +use regex::Regex; use reqwest::Url; use crate::{ @@ -22,6 +23,21 @@ pub static CONFIG: Lazy = Lazy::new(|| { }) }); +static PRIVACY_REGEX: Lazy = Lazy::new(|| Regex::new(r"[\w]").unwrap()); +const PRIVACY_CONFIG: &[&str] = &[ + "allowed_iframe_ancestors", + "database_url", + "domain_origin", + "domain_path", + "domain", + "helo_name", + "org_creation_users", + "signups_domains_whitelist", + "smtp_from", + "smtp_host", + "smtp_username", +]; + pub type Pass = String; macro_rules! make_config { @@ -52,6 +68,7 @@ macro_rules! make_config { } impl ConfigBuilder { + #[allow(clippy::field_reassign_with_default)] fn from_env() -> Self { match dotenv::from_path(".env") { Ok(_) => (), @@ -196,8 +213,37 @@ macro_rules! make_config { }, )+ ]}, )+ ]) } + + pub fn get_support_json(&self) -> serde_json::Value { + let cfg = { + let inner = &self.inner.read().unwrap(); + inner.config.clone() + }; + + json!({ $($( + stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action }, + )+)+ }) + } + } + }; + + // Support string print + ( @supportstr $name:ident, $value:expr, Pass, option ) => { $value.as_ref().map(|_| String::from("***")) }; // Optional pass, we map to an Option with "***" + ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { String::from("***") }; // Required pass, we return "***" + ( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config + if PRIVACY_CONFIG.contains(&stringify!($name)) { + json!($value.as_ref().map(|x| PRIVACY_REGEX.replace_all(&x.to_string(), "${1}*").to_string())) + } else { + json!($value) } }; + ( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config + if PRIVACY_CONFIG.contains(&stringify!($name)) { + json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string()) + } else { + json!($value) + } + }; // Group or empty string ( @show ) => { "" }; @@ -458,7 +504,6 @@ make_config! { } fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { - // Validate connection URL is valid and DB feature is enabled DbConnType::from_url(&cfg.database_url)?; @@ -472,7 +517,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { let dom = cfg.domain.to_lowercase(); if !dom.starts_with("http://") && !dom.starts_with("https://") { - err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'"); + err!( + "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'" + ); } let whitelist = &cfg.signups_domains_whitelist; @@ -481,10 +528,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } let org_creation_users = cfg.org_creation_users.trim().to_lowercase(); - if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") { - if org_creation_users.split(',').any(|u| !u.contains('@')) { - err!("`ORG_CREATION_USERS` contains invalid email addresses"); - } + if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") + && org_creation_users.split(',').any(|u| !u.contains('@')) + { + err!("`ORG_CREATION_USERS` contains invalid email addresses"); } if let Some(ref token) = cfg.admin_token { @@ -529,7 +576,6 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { // Check if the icon blacklist regex is valid if let Some(ref r) = cfg.icon_blacklist_regex { - use regex::Regex; let validate_regex = Regex::new(&r); match validate_regex { Ok(_) => (), @@ -577,7 +623,12 @@ impl Config { validate_config(&config)?; Ok(Config { - inner: RwLock::new(Inner { templates: load_templates(&config.templates_folder), config, _env, _usr }), + inner: RwLock::new(Inner { + templates: load_templates(&config.templates_folder), + config, + _env, + _usr, + }), }) } @@ -650,7 +701,7 @@ impl Config { /// Tests whether the specified user is allowed to create an organization. pub fn is_org_creation_allowed(&self, email: &str) -> bool { let users = self.org_creation_users(); - if users == "" || users == "all" { + if users.is_empty() || users == "all" { true } else if users == "none" { false @@ -704,8 +755,10 @@ impl Config { let akey_s = data_encoding::BASE64.encode(&akey); // Save the new value - let mut builder = ConfigBuilder::default(); - builder._duo_akey = Some(akey_s.clone()); + let builder = ConfigBuilder { + _duo_akey: Some(akey_s.clone()), + ..Default::default() + }; self.update_config_partial(builder).ok(); akey_s diff --git a/src/main.rs b/src/main.rs index f8d88af4..3099aabd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] #![cfg_attr(feature = "unstable", feature(ip))] -#![recursion_limit = "256"] +#![recursion_limit = "512"] extern crate openssl; #[macro_use] diff --git a/src/static/images/hibp.png b/src/static/images/hibp.png index bf90bbf2..99d485a5 100644 Binary files a/src/static/images/hibp.png and b/src/static/images/hibp.png differ diff --git a/src/static/images/logo-gray.png b/src/static/images/logo-gray.png index 70658e18..57b2949e 100644 Binary files a/src/static/images/logo-gray.png and b/src/static/images/logo-gray.png differ diff --git a/src/static/images/mail-github.png b/src/static/images/mail-github.png index 07935f11..fa89cac5 100644 Binary files a/src/static/images/mail-github.png and b/src/static/images/mail-github.png differ diff --git a/src/static/images/shield-white.png b/src/static/images/shield-white.png index 3400efe7..ae5db28a 100644 Binary files a/src/static/images/shield-white.png and b/src/static/images/shield-white.png differ diff --git a/src/static/scripts/bootstrap-native.js b/src/static/scripts/bootstrap-native.js index cfb6ae3f..8fafa16a 100644 --- a/src/static/scripts/bootstrap-native.js +++ b/src/static/scripts/bootstrap-native.js @@ -1,6 +1,6 @@ /*! - * Native JavaScript for Bootstrap v3.0.10 (https://thednp.github.io/bootstrap.native/) - * Copyright 2015-2020 © dnp_theme + * Native JavaScript for Bootstrap v3.0.15 (https://thednp.github.io/bootstrap.native/) + * Copyright 2015-2021 © dnp_theme * Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE) */ (function (global, factory) { @@ -15,10 +15,14 @@ var transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration'; + var transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty'; + function getElementTransitionDuration(element) { - var duration = supportTransition ? parseFloat(getComputedStyle(element)[transitionDuration]) : 0; - duration = typeof duration === 'number' && !isNaN(duration) ? duration * 1000 : 0; - return duration; + var computedStyle = getComputedStyle(element), + property = computedStyle[transitionProperty], + duration = supportTransition && property && property !== 'none' + ? parseFloat(computedStyle[transitionDuration]) : 0; + return !isNaN(duration) ? duration * 1000 : 0; } function emulateTransitionEnd(element,handler){ @@ -35,9 +39,15 @@ return selector instanceof Element ? selector : lookUp.querySelector(selector); } - function bootstrapCustomEvent(eventName, componentName, related) { + function bootstrapCustomEvent(eventName, componentName, eventProperties) { var OriginalCustomEvent = new CustomEvent( eventName + '.bs.' + componentName, {cancelable: true}); - OriginalCustomEvent.relatedTarget = related; + if (typeof eventProperties !== 'undefined') { + Object.keys(eventProperties).forEach(function (key) { + Object.defineProperty(OriginalCustomEvent, key, { + value: eventProperties[key] + }); + }); + } return OriginalCustomEvent; } @@ -352,7 +362,7 @@ }; self.slideTo = function (next) { if (vars.isSliding) { return; } - var activeItem = self.getActiveIndex(), orientation; + var activeItem = self.getActiveIndex(), orientation, eventProperties; if ( activeItem === next ) { return; } else if ( (activeItem < next ) || (activeItem === 0 && next === slides.length -1 ) ) { @@ -363,8 +373,9 @@ if ( next < 0 ) { next = slides.length - 1; } else if ( next >= slides.length ){ next = 0; } orientation = vars.direction === 'left' ? 'next' : 'prev'; - slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', slides[next]); - slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', slides[next]); + eventProperties = { relatedTarget: slides[next], direction: vars.direction, from: activeItem, to: next }; + slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', eventProperties); + slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', eventProperties); dispatchCustomEvent.call(element, slideCustomEvent); if (slideCustomEvent.defaultPrevented) { return; } vars.index = next; @@ -615,7 +626,7 @@ } } self.show = function () { - showCustomEvent = bootstrapCustomEvent('show', 'dropdown', relatedTarget); + showCustomEvent = bootstrapCustomEvent('show', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, showCustomEvent); if ( showCustomEvent.defaultPrevented ) { return; } menu.classList.add('show'); @@ -626,12 +637,12 @@ setTimeout(function () { setFocus( menu.getElementsByTagName('INPUT')[0] || element ); toggleDismiss(); - shownCustomEvent = bootstrapCustomEvent( 'shown', 'dropdown', relatedTarget); + shownCustomEvent = bootstrapCustomEvent('shown', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, shownCustomEvent); },1); }; self.hide = function () { - hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', relatedTarget); + hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, hideCustomEvent); if ( hideCustomEvent.defaultPrevented ) { return; } menu.classList.remove('show'); @@ -643,7 +654,7 @@ setTimeout(function () { element.Dropdown && element.addEventListener('click',clickHandler,false); },1); - hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', relatedTarget); + hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, hiddenCustomEvent); }; self.toggle = function () { @@ -749,7 +760,7 @@ setFocus(modal); modal.isAnimating = false; toggleEvents(1); - shownCustomEvent = bootstrapCustomEvent('shown', 'modal', relatedTarget); + shownCustomEvent = bootstrapCustomEvent('shown', 'modal', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(modal, shownCustomEvent); } function triggerHide(force) { @@ -804,7 +815,7 @@ }; self.show = function () { if (modal.classList.contains('show') && !!modal.isAnimating ) {return} - showCustomEvent = bootstrapCustomEvent('show', 'modal', relatedTarget); + showCustomEvent = bootstrapCustomEvent('show', 'modal', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(modal, showCustomEvent); if ( showCustomEvent.defaultPrevented ) { return; } modal.isAnimating = true; @@ -1193,7 +1204,7 @@ if (dropLink && !dropLink.classList.contains('active') ) { dropLink.classList.add('active'); } - dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', vars.items[index])); + dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', { relatedTarget: vars.items[index] })); } else if ( isActive && !inside ) { item.classList.remove('active'); if (dropLink && dropLink.classList.contains('active') && !item.parentNode.getElementsByClassName('active').length ) { @@ -1278,7 +1289,7 @@ } else { tabs.isAnimating = false; } - shownCustomEvent = bootstrapCustomEvent('shown', 'tab', activeTab); + shownCustomEvent = bootstrapCustomEvent('shown', 'tab', { relatedTarget: activeTab }); dispatchCustomEvent.call(next, shownCustomEvent); } function triggerHide() { @@ -1287,8 +1298,8 @@ nextContent.style.float = 'left'; containerHeight = activeContent.scrollHeight; } - showCustomEvent = bootstrapCustomEvent('show', 'tab', activeTab); - hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', next); + showCustomEvent = bootstrapCustomEvent('show', 'tab', { relatedTarget: activeTab }); + hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', { relatedTarget: next }); dispatchCustomEvent.call(next, showCustomEvent); if ( showCustomEvent.defaultPrevented ) { return; } nextContent.classList.add('active'); @@ -1331,7 +1342,7 @@ nextContent = queryElement(next.getAttribute('href')); activeTab = getActiveTab(); activeContent = getActiveContent(); - hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', next); + hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', { relatedTarget: next }); dispatchCustomEvent.call(activeTab, hideCustomEvent); if (hideCustomEvent.defaultPrevented) { return; } tabs.isAnimating = true; @@ -1637,7 +1648,7 @@ } } - var version = "3.0.10"; + var version = "3.0.15"; var index = { Alert: Alert, diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css index 0bb38131..a3171bef 100644 --- a/src/static/scripts/bootstrap.css +++ b/src/static/scripts/bootstrap.css @@ -1,10 +1,10 @@ /*! - * Bootstrap v4.5.2 (https://getbootstrap.com/) + * Bootstrap v4.5.3 (https://getbootstrap.com/) * Copyright 2011-2020 The Bootstrap Authors * Copyright 2011-2020 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ - :root { +:root { --blue: #007bff; --indigo: #6610f2; --purple: #6f42c1; @@ -216,6 +216,7 @@ caption { th { text-align: inherit; + text-align: -webkit-match-parent; } label { @@ -3750,6 +3751,8 @@ input[type="button"].btn-block { display: block; min-height: 1.5rem; padding-left: 1.5rem; + -webkit-print-color-adjust: exact; + color-adjust: exact; } .custom-control-inline { @@ -5289,6 +5292,7 @@ a.badge-dark:focus, a.badge-dark.focus { position: absolute; top: 0; right: 0; + z-index: 2; padding: 0.75rem 1.25rem; color: inherit; } @@ -10163,7 +10167,7 @@ a.text-dark:hover, a.text-dark:focus { .text-break { word-break: break-word !important; - overflow-wrap: break-word !important; + word-wrap: break-word !important; } .text-reset { @@ -10256,3 +10260,4 @@ a.text-dark:hover, a.text-dark:focus { border-color: #dee2e6; } } +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index 1c464bbd..590d541a 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,12 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/dt-1.10.22 + * https://datatables.net/download/#bs4/dt-1.10.23 * * Included libraries: - * DataTables 1.10.22 + * DataTables 1.10.23 */ +@charset "UTF-8"; table.dataTable { clear: both; margin-top: 6px !important; @@ -114,7 +115,7 @@ table.dataTable > thead .sorting_desc:before, table.dataTable > thead .sorting_asc_disabled:before, table.dataTable > thead .sorting_desc_disabled:before { right: 1em; - content: "\2191"; + content: "↑"; } table.dataTable > thead .sorting:after, table.dataTable > thead .sorting_asc:after, @@ -122,7 +123,7 @@ table.dataTable > thead .sorting_desc:after, table.dataTable > thead .sorting_asc_disabled:after, table.dataTable > thead .sorting_desc_disabled:after { right: 0.5em; - content: "\2193"; + content: "↓"; } table.dataTable > thead .sorting_asc:before, table.dataTable > thead .sorting_desc:after { @@ -165,9 +166,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { @media screen and (max-width: 767px) { div.dataTables_wrapper div.dataTables_length, - div.dataTables_wrapper div.dataTables_filter, - div.dataTables_wrapper div.dataTables_info, - div.dataTables_wrapper div.dataTables_paginate { +div.dataTables_wrapper div.dataTables_filter, +div.dataTables_wrapper div.dataTables_info, +div.dataTables_wrapper div.dataTables_paginate { text-align: center; } div.dataTables_wrapper div.dataTables_paginate ul.pagination { @@ -213,10 +214,10 @@ div.dataTables_scrollHead table.table-bordered { div.table-responsive > div.dataTables_wrapper > div.row { margin: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { +div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child { padding-left: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { +div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child { padding-right: 0; } diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 7f898cd6..e25c916c 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/dt-1.10.22 + * https://datatables.net/download/#bs4/dt-1.10.23 * * Included libraries: - * DataTables 1.10.22 + * DataTables 1.10.23 */ -/*! DataTables 1.10.22 +/*! DataTables 1.10.23 * ©2008-2020 SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.10.22 + * @version 1.10.23 * @file jquery.dataTables.js * @author SpryMedia Ltd * @contact www.datatables.net @@ -2775,7 +2775,7 @@ for ( var i=0, iLen=a.length-1 ; i + Bitwarden_rs Admin Panel