From 1b64b9e1644d5e07d674d29d83c5e7e58e8f0a2b Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 3 Dec 2022 18:25:59 +0100 Subject: [PATCH 1/5] Add dev-only query logging support This PR adds query logging support as an optional feature. It is only allowed during development/debug builds, and will abort when used during a `--release` build. For this feature to be fully activated you also need to se an environment variable `QUERY_LOGGER=1` to activate the debug log-level for this crate, else there will be no output. The reason for this PR is that sometimes it is useful to be able to see the generated queries, like when debugging an issue, or trying to optimize a query. Currently i always added this code when needed, but having this a part of the code could benifit other developers too who maybe need this. --- Cargo.lock | 11 +++++++++++ Cargo.toml | 6 ++++++ build.rs | 21 +++++++++++++-------- src/db/mod.rs | 8 ++++++++ src/main.rs | 8 ++++++++ 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfe9bc14..c402442d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,16 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel_logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22b1f4804a69ed8954910b2ab30dedc759665e0284e57db95eef4a7b5edffb" +dependencies = [ + "diesel", + "log", +] + [[package]] name = "diesel_migrations" version = "2.0.0" @@ -3180,6 +3190,7 @@ dependencies = [ "data-encoding", "data-url", "diesel", + "diesel_logger", "diesel_migrations", "dotenvy", "email_address", diff --git a/Cargo.toml b/Cargo.toml index 1f3930c7..c667ab90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,11 @@ vendored_openssl = ["openssl/vendored"] # Enable MiMalloc memory allocator to replace the default malloc # This can improve performance for Alpine builds enable_mimalloc = ["mimalloc"] +# This is a development dependency, and should only be used during development! +# It enables the usage of the diesel_logger crate, which is able to output the generated queries. +# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile +# if you want to turn off the logging for a specific run. +query_logger = ["diesel_logger"] # Enable unstable features, requires nightly # Currently only used to enable rusts official ip support @@ -70,6 +75,7 @@ serde_json = "1.0.87" # A safe, extensible ORM and Query builder diesel = { version = "2.0.2", features = ["chrono", "r2d2"] } diesel_migrations = "2.0.0" +diesel_logger = { version = "0.2.0", optional = true } # Bundled SQLite libsqlite3-sys = { version = "0.25.2", features = ["bundled"], optional = true } diff --git a/build.rs b/build.rs index 7d0a7bce..1b171fb4 100644 --- a/build.rs +++ b/build.rs @@ -9,20 +9,25 @@ fn main() { println!("cargo:rustc-cfg=mysql"); #[cfg(feature = "postgresql")] println!("cargo:rustc-cfg=postgresql"); + #[cfg(feature = "query_logger")] + println!("cargo:rustc-cfg=query_logger"); #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))] compile_error!( "You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite" ); + #[cfg(all(not(debug_assertions), feature = "query_logger"))] + compile_error!("Query Logging is only allowed during development, it is not intented for production usage!"); + // Support $BWRS_VERSION for legacy compatibility, but default to $VW_VERSION. // If neither exist, read from git. let maybe_vaultwarden_version = env::var("VW_VERSION").or_else(|_| env::var("BWRS_VERSION")).or_else(|_| version_from_git_info()); if let Ok(version) = maybe_vaultwarden_version { - println!("cargo:rustc-env=VW_VERSION={}", version); - println!("cargo:rustc-env=CARGO_PKG_VERSION={}", version); + println!("cargo:rustc-env=VW_VERSION={version}"); + println!("cargo:rustc-env=CARGO_PKG_VERSION={version}"); } } @@ -47,29 +52,29 @@ fn version_from_git_info() -> Result { // the current commit doesn't have an associated tag let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok(); if let Some(ref exact) = exact_tag { - println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact); + println!("cargo:rustc-env=GIT_EXACT_TAG={exact}"); } // The last available tag, equal to exact_tag when // the current commit is tagged let last_tag = run(&["git", "describe", "--abbrev=0", "--tags"])?; - println!("cargo:rustc-env=GIT_LAST_TAG={}", last_tag); + println!("cargo:rustc-env=GIT_LAST_TAG={last_tag}"); // The current branch name let branch = run(&["git", "rev-parse", "--abbrev-ref", "HEAD"])?; - println!("cargo:rustc-env=GIT_BRANCH={}", branch); + println!("cargo:rustc-env=GIT_BRANCH={branch}"); // The current git commit hash let rev = run(&["git", "rev-parse", "HEAD"])?; let rev_short = rev.get(..8).unwrap_or_default(); - println!("cargo:rustc-env=GIT_REV={}", rev_short); + println!("cargo:rustc-env=GIT_REV={rev_short}"); // Combined version if let Some(exact) = exact_tag { Ok(exact) } else if &branch != "main" && &branch != "master" { - Ok(format!("{}-{} ({})", last_tag, rev_short, branch)) + Ok(format!("{last_tag}-{rev_short} ({branch})")) } else { - Ok(format!("{}-{}", last_tag, rev_short)) + Ok(format!("{last_tag}-{rev_short}")) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index c2570d9d..b04e7e7d 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -182,12 +182,20 @@ macro_rules! generate_connections { }; } +#[cfg(not(query_logger))] generate_connections! { sqlite: diesel::sqlite::SqliteConnection, mysql: diesel::mysql::MysqlConnection, postgresql: diesel::pg::PgConnection } +#[cfg(query_logger)] +generate_connections! { + sqlite: diesel_logger::LoggingConnection, + mysql: diesel_logger::LoggingConnection, + postgresql: diesel_logger::LoggingConnection +} + impl DbConnType { pub fn from_url(url: &str) -> Result { // Mysql diff --git a/src/main.rs b/src/main.rs index 86bcc36c..08e223b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,6 +171,13 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { log::LevelFilter::Off }; + let diesel_logger_level: log::LevelFilter = + if cfg!(feature = "query_logger") && std::env::var("QUERY_LOGGER").is_ok() { + log::LevelFilter::Debug + } else { + log::LevelFilter::Off + }; + let mut logger = fern::Dispatch::new() .level(level) // Hide unknown certificate errors if using self-signed @@ -191,6 +198,7 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { .level_for("cookie_store", log::LevelFilter::Off) // Variable level for trust-dns used by reqwest .level_for("trust_dns_proto", trust_dns_level) + .level_for("diesel_logger", diesel_logger_level) .chain(std::io::stdout()); // Enable smtp debug logging only specifically for smtp when need. From c0e3c2c5e14bf28252c5c0ae6f1552561ac4b863 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 26 Nov 2022 19:07:28 +0100 Subject: [PATCH 2/5] Cleanups and Fixes for Emergency Access - Several cleanups and code optimizations for Emergency Access - Fixed a race-condition regarding jobs for Emergency Access - Some other small changes like `allow(clippy::)` removals Fixes #2925 --- .env.template | 8 +- src/api/admin.rs | 2 +- src/api/core/emergency_access.rs | 191 ++++++++++++++++-------------- src/api/core/organizations.rs | 4 +- src/api/icons.rs | 1 - src/auth.rs | 12 +- src/config.rs | 8 +- src/db/mod.rs | 1 - src/db/models/emergency_access.rs | 78 +++++++----- src/db/models/user.rs | 2 +- src/error.rs | 1 - src/mail.rs | 18 +-- 12 files changed, 173 insertions(+), 153 deletions(-) diff --git a/.env.template b/.env.template index 736b6463..22877f15 100644 --- a/.env.template +++ b/.env.template @@ -119,12 +119,12 @@ # INCOMPLETE_2FA_SCHEDULE="30 * * * * *" ## ## Cron schedule of the job that sends expiration reminders to emergency access grantors. -## Defaults to hourly (5 minutes after the hour). Set blank to disable this job. -# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *" +## Defaults to hourly (3 minutes after the hour). Set blank to disable this job. +# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 3 * * * *" ## ## Cron schedule of the job that grants emergency access requests that have met the required wait time. -## Defaults to hourly (5 minutes after the hour). Set blank to disable this job. -# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 5 * * * *" +## Defaults to hourly (7 minutes after the hour). Set blank to disable this job. +# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 7 * * * *" ## ## Cron schedule of the job that cleans old events from the event table. ## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start. diff --git a/src/api/admin.rs b/src/api/admin.rs index 656f63bf..6c908bfc 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -284,7 +284,7 @@ async fn invite_user(data: Json, _token: AdminToken, mut conn: DbCon if CONFIG.mail_enabled() { mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await } else { - let invitation = Invitation::new(user.email.clone()); + let invitation = Invitation::new(&user.email); invitation.save(conn).await } } diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 7c3b09c5..7a683ea4 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -1,6 +1,5 @@ use chrono::{Duration, Utc}; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{serde::json::Json, Route}; use serde_json::Value; use crate::{ @@ -41,9 +40,10 @@ pub fn routes() -> Vec { async fn get_contacts(headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let mut emergency_access_list_json = Vec::new(); - for e in EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await { - emergency_access_list_json.push(e.to_json_grantee_details(&mut conn).await); + let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await; + let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); + for ea in emergency_access_list { + emergency_access_list_json.push(ea.to_json_grantee_details(&mut conn).await); } Ok(Json(json!({ @@ -57,9 +57,10 @@ async fn get_contacts(headers: Headers, mut conn: DbConn) -> JsonResult { async fn get_grantees(headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let mut emergency_access_list_json = Vec::new(); - for e in EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await { - emergency_access_list_json.push(e.to_json_grantor_details(&mut conn).await); + let emergency_access_list = EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await; + let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); + for ea in emergency_access_list { + emergency_access_list_json.push(ea.to_json_grantor_details(&mut conn).await); } Ok(Json(json!({ @@ -83,7 +84,7 @@ async fn get_emergency_access(emer_id: String, mut conn: DbConn) -> JsonResult { // region put/post -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct EmergencyAccessUpdateData { Type: NumberOrString, @@ -160,7 +161,7 @@ async fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: D // region invite -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct EmergencyAccessInviteData { Email: String, @@ -193,7 +194,7 @@ async fn send_invite(data: JsonUpcase, headers: Heade let grantee_user = match User::find_by_mail(&email, &mut conn).await { None => { if !CONFIG.invitations_allowed() { - err!(format!("Grantee user does not exist: {}", email)) + err!(format!("Grantee user does not exist: {}", &email)) } if !CONFIG.is_email_domain_allowed(&email) { @@ -201,7 +202,7 @@ async fn send_invite(data: JsonUpcase, headers: Heade } if !CONFIG.mail_enabled() { - let invitation = Invitation::new(email.clone()); + let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } @@ -221,36 +222,29 @@ async fn send_invite(data: JsonUpcase, headers: Heade .await .is_some() { - err!(format!("Grantee user already invited: {}", email)) + err!(format!("Grantee user already invited: {}", &grantee_user.email)) } - let mut new_emergency_access = EmergencyAccess::new( - grantor_user.uuid.clone(), - Some(grantee_user.email.clone()), - emergency_access_status, - new_type, - wait_time_days, - ); + let mut new_emergency_access = + EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days); new_emergency_access.save(&mut conn).await?; if CONFIG.mail_enabled() { mail::send_emergency_access_invite( - &grantee_user.email, + &new_emergency_access.email.expect("Grantee email does not exists"), &grantee_user.uuid, - Some(new_emergency_access.uuid), - Some(grantor_user.name.clone()), - Some(grantor_user.email), + &new_emergency_access.uuid, + &grantor_user.name, + &grantor_user.email, ) .await?; } else { // Automatically mark user as accepted if no email invites match User::find_by_mail(&email, &mut conn).await { - Some(user) => { - match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), &mut conn).await { - Ok(v) => v, - Err(e) => err!(e.to_string()), - } - } + Some(user) => match accept_invite_process(user.uuid, &mut new_emergency_access, &email, &mut conn).await { + Ok(v) => v, + Err(e) => err!(e.to_string()), + }, None => err!("Grantee user not found."), } } @@ -262,7 +256,7 @@ async fn send_invite(data: JsonUpcase, headers: Heade async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult { check_emergency_access_allowed()?; - let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { + let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; @@ -291,19 +285,19 @@ async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> E mail::send_emergency_access_invite( &email, &grantor_user.uuid, - Some(emergency_access.uuid), - Some(grantor_user.name.clone()), - Some(grantor_user.email), + &emergency_access.uuid, + &grantor_user.name, + &grantor_user.email, ) .await?; } else { if Invitation::find_by_mail(&email, &mut conn).await.is_none() { - let invitation = Invitation::new(email); + let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } // Automatically mark user as accepted if no email invites - match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, &mut conn).await { + match accept_invite_process(grantee_user.uuid, &mut emergency_access, &email, &mut conn).await { Ok(v) => v, Err(e) => err!(e.to_string()), } @@ -319,13 +313,24 @@ struct AcceptData { } #[post("/emergency-access//accept", data = "")] -async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: DbConn) -> EmptyResult { +async fn accept_invite( + emer_id: String, + data: JsonUpcase, + headers: Headers, + mut conn: DbConn, +) -> EmptyResult { check_emergency_access_allowed()?; let data: AcceptData = data.into_inner().data; let token = &data.Token; let claims = decode_emergency_access_invite(token)?; + // This can happen if the user who received the invite used a different email to signup. + // Since we do not know if this is intented, we error out here and do nothing with the invite. + if claims.email != headers.user.email { + err!("Claim email does not match current users email") + } + let grantee_user = match User::find_by_mail(&claims.email, &mut conn).await { Some(user) => { Invitation::take(&claims.email, &mut conn).await; @@ -334,7 +339,7 @@ async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: None => err!("Invited user not found"), }; - let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { + let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; @@ -345,13 +350,11 @@ async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: None => err!("Grantor user not found."), }; - if (claims.emer_id.is_some() && emer_id == claims.emer_id.unwrap()) - && (claims.grantor_name.is_some() && grantor_user.name == claims.grantor_name.unwrap()) - && (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap()) + if emer_id == claims.emer_id + && grantor_user.name == claims.grantor_name + && grantor_user.email == claims.grantor_email { - match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &mut conn) - .await - { + match accept_invite_process(grantee_user.uuid, &mut emergency_access, &grantee_user.email, &mut conn).await { Ok(v) => v, Err(e) => err!(e.to_string()), } @@ -368,17 +371,11 @@ async fn accept_invite(emer_id: String, data: JsonUpcase, mut conn: async fn accept_invite_process( grantee_uuid: String, - emer_id: String, - email: Option, + emergency_access: &mut EmergencyAccess, + grantee_email: &str, conn: &mut DbConn, ) -> EmptyResult { - let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - let emer_email = emergency_access.email; - if emer_email.is_none() || emer_email != email { + if emergency_access.email.is_none() || emergency_access.email.as_ref().unwrap() != grantee_email { err!("User email does not match invite."); } @@ -463,7 +460,7 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: }; if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 - || emergency_access.grantee_uuid != Some(initiating_user.uuid.clone()) + || emergency_access.grantee_uuid != Some(initiating_user.uuid) { err!("Emergency access not valid.") } @@ -485,7 +482,7 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: &grantor_user.email, &initiating_user.name, emergency_access.get_type_as_str(), - &emergency_access.wait_time_days.clone().to_string(), + &emergency_access.wait_time_days, ) .await?; } @@ -496,19 +493,18 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let approving_user = headers.user; let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 - || emergency_access.grantor_uuid != approving_user.uuid + || emergency_access.grantor_uuid != headers.user.uuid { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&approving_user.uuid, &mut conn).await { + let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { Some(user) => user, None => err!("Grantor user not found."), }; @@ -535,7 +531,6 @@ async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: D async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let rejecting_user = headers.user; let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), @@ -543,12 +538,12 @@ async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: Db if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 && emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32) - || emergency_access.grantor_uuid != rejecting_user.uuid + || emergency_access.grantor_uuid != headers.user.uuid { err!("Emergency access not valid.") } - let grantor_user = match User::find_by_uuid(&rejecting_user.uuid, &mut conn).await { + let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { Some(user) => user, None => err!("Grantor user not found."), }; @@ -579,14 +574,12 @@ async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: Db async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult { check_emergency_access_allowed()?; - let requesting_user = headers.user; - let host = headers.host; let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await { Some(emer) => emer, None => err!("Emergency access not valid."), }; - if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::View) { + if !is_valid_request(&emergency_access, headers.user.uuid, EmergencyAccessType::View) { err!("Emergency access not valid.") } @@ -596,7 +589,8 @@ async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbCo let mut ciphers_json = Vec::new(); for c in ciphers { - ciphers_json.push(c.to_json(&host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await); + ciphers_json + .push(c.to_json(&headers.host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await); } Ok(Json(json!({ @@ -633,7 +627,7 @@ async fn takeover_emergency_access(emer_id: String, headers: Headers, mut conn: }))) } -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct EmergencyAccessPasswordData { NewMasterPasswordHash: String, @@ -738,40 +732,44 @@ pub async fn emergency_request_timeout_job(pool: DbPool) { } if let Ok(mut conn) = pool.get().await { - let emergency_access_list = EmergencyAccess::find_all_recoveries(&mut conn).await; + let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await; if emergency_access_list.is_empty() { debug!("No emergency request timeout to approve"); } + let now = Utc::now().naive_utc(); for mut emer in emergency_access_list { - if emer.recovery_initiated_at.is_some() - && Utc::now().naive_utc() - >= emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days)) - { - emer.status = EmergencyAccessStatus::RecoveryApproved as i32; - emer.save(&mut conn).await.expect("Cannot save emergency access on job"); + // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None) + let recovery_allowed_at = + emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days)); + if recovery_allowed_at.le(&now) { + // Only update the access status + // Updating the whole record could cause issues when the emergency_notification_reminder_job is also active + emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &mut conn) + .await + .expect("Unable to update emergency access status"); if CONFIG.mail_enabled() { // get grantor user to send Accepted email let grantor_user = - User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found."); + User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found"); // get grantee user to send Accepted email let grantee_user = - User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &mut conn) + User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn) .await - .expect("Grantee user not found."); + .expect("Grantee user not found"); mail::send_emergency_access_recovery_timed_out( &grantor_user.email, - &grantee_user.name.clone(), + &grantee_user.name, emer.get_type_as_str(), ) .await .expect("Error on sending email"); - mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name.clone()) + mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name) .await .expect("Error on sending email"); } @@ -789,38 +787,47 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) { } if let Ok(mut conn) = pool.get().await { - let emergency_access_list = EmergencyAccess::find_all_recoveries(&mut conn).await; + let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await; if emergency_access_list.is_empty() { debug!("No emergency request reminder notification to send"); } + let now = Utc::now().naive_utc(); for mut emer in emergency_access_list { - if (emer.recovery_initiated_at.is_some() - && Utc::now().naive_utc() - >= emer.recovery_initiated_at.unwrap() + Duration::days((i64::from(emer.wait_time_days)) - 1)) - && (emer.last_notification_at.is_none() - || (emer.last_notification_at.is_some() - && Utc::now().naive_utc() >= emer.last_notification_at.unwrap() + Duration::days(1))) - { - emer.save(&mut conn).await.expect("Cannot save emergency access on job"); + // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None) + // Calculate the day before the recovery will become active + let final_recovery_reminder_at = + emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days - 1)); + // Calculate if a day has passed since the previous notification, else no notification has been sent before + let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at { + last_notification_at + Duration::days(1) + } else { + now + }; + if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) { + // Only update the last notification date + // Updating the whole record could cause issues when the emergency_request_timeout_job is also active + emer.update_last_notification_date_and_save(&now, &mut conn) + .await + .expect("Unable to update emergency access notification date"); if CONFIG.mail_enabled() { // get grantor user to send Accepted email let grantor_user = - User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found."); + User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found"); // get grantee user to send Accepted email let grantee_user = - User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &mut conn) + User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn) .await - .expect("Grantee user not found."); + .expect("Grantee user not found"); mail::send_emergency_access_recovery_reminder( &grantor_user.email, - &grantee_user.name.clone(), + &grantee_user.name, emer.get_type_as_str(), - &emer.wait_time_days.to_string(), // TODO(jjlin): This should be the number of days left. + "1", // This notification is only triggered one day before the activation ) .await .expect("Error on sending email"); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 57a982f9..b612ccc3 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -721,7 +721,7 @@ async fn send_invite( } if !CONFIG.mail_enabled() { - let invitation = Invitation::new(email.clone()); + let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } @@ -871,7 +871,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co ) .await?; } else { - let invitation = Invitation::new(user.email); + let invitation = Invitation::new(&user.email); invitation.save(conn).await?; } diff --git a/src/api/icons.rs b/src/api/icons.rs index a69b7359..509e88c0 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -260,7 +260,6 @@ mod tests { use cached::proc_macro::cached; #[cached(key = "String", convert = r#"{ domain.to_string() }"#, size = 16, time = 60)] -#[allow(clippy::unused_async)] // This is needed because cached causes a false-positive here. async fn is_domain_blacklisted(domain: &str) -> bool { // First check the blacklist regex if there is a match. // This prevents the blocked domain(s) from being leaked via a DNS lookup. diff --git a/src/auth.rs b/src/auth.rs index 0db2d95a..8dea4165 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -177,17 +177,17 @@ pub struct EmergencyAccessInviteJwtClaims { pub sub: String, pub email: String, - pub emer_id: Option, - pub grantor_name: Option, - pub grantor_email: Option, + pub emer_id: String, + pub grantor_name: String, + pub grantor_email: String, } pub fn generate_emergency_access_invite_claims( uuid: String, email: String, - emer_id: Option, - grantor_name: Option, - grantor_email: Option, + emer_id: String, + grantor_name: String, + grantor_email: String, ) -> EmergencyAccessInviteJwtClaims { let time_now = Utc::now().naive_utc(); let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); diff --git a/src/config.rs b/src/config.rs index fe98d2df..fbf0e412 100644 --- a/src/config.rs +++ b/src/config.rs @@ -366,11 +366,11 @@ make_config! { /// Defaults to once every minute. Set blank to disable this job. incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string(); /// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors. - /// Defaults to hourly. Set blank to disable this job. - emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string(); + /// Defaults to hourly. (3 minutes after the hour) Set blank to disable this job. + emergency_notification_reminder_schedule: String, false, def, "0 3 * * * *".to_string(); /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time. - /// Defaults to hourly. Set blank to disable this job. - emergency_request_timeout_schedule: String, false, def, "0 5 * * * *".to_string(); + /// Defaults to hourly. (7 minutes after the hour) Set blank to disable this job. + emergency_request_timeout_schedule: String, false, def, "0 7 * * * *".to_string(); /// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table. /// Defaults to daily. Set blank to disable this job. event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_string(); diff --git a/src/db/mod.rs b/src/db/mod.rs index b04e7e7d..ef30ffe7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -125,7 +125,6 @@ macro_rules! generate_connections { impl DbPool { // For the given database URL, guess its type, run migrations, create pool, and return it - #[allow(clippy::diverging_sub_expression)] pub fn from_config() -> Result { let url = CONFIG.database_url(); let conn_type = DbConnType::from_url(&url)?; diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 3971fa04..ccb21e5b 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -1,10 +1,12 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; +use crate::{api::EmptyResult, db::DbConn, error::MapResult}; + use super::User; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = emergency_access)] #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] @@ -27,14 +29,14 @@ db_object! { /// Local methods impl EmergencyAccess { - pub fn new(grantor_uuid: String, email: Option, status: i32, atype: i32, wait_time_days: i32) -> Self { + pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { let now = Utc::now().naive_utc(); Self { uuid: crate::util::get_uuid(), grantor_uuid, grantee_uuid: None, - email, + email: Some(email), status, atype, wait_time_days, @@ -54,14 +56,6 @@ impl EmergencyAccess { } } - pub fn has_type(&self, access_type: EmergencyAccessType) -> bool { - self.atype == access_type as i32 - } - - pub fn has_status(&self, status: EmergencyAccessStatus) -> bool { - self.status == status as i32 - } - pub fn to_json(&self) -> Value { json!({ "Id": self.uuid, @@ -87,7 +81,6 @@ impl EmergencyAccess { }) } - #[allow(clippy::manual_map)] pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Value { let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() { Some(User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")) @@ -110,7 +103,7 @@ impl EmergencyAccess { } } -#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] +#[derive(Copy, Clone)] pub enum EmergencyAccessType { View = 0, Takeover = 1, @@ -126,18 +119,6 @@ impl EmergencyAccessType { } } -impl PartialEq for EmergencyAccessType { - fn eq(&self, other: &i32) -> bool { - *other == *self as i32 - } -} - -impl PartialEq for i32 { - fn eq(&self, other: &EmergencyAccessType) -> bool { - *self == *other as i32 - } -} - pub enum EmergencyAccessStatus { Invited = 0, Accepted = 1, @@ -148,11 +129,6 @@ pub enum EmergencyAccessStatus { // region Database methods -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - impl EmergencyAccess { pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult { User::update_uuid_revision(&self.grantor_uuid, conn).await; @@ -189,6 +165,45 @@ impl EmergencyAccess { } } + pub async fn update_access_status_and_save( + &mut self, + status: i32, + date: &NaiveDateTime, + conn: &mut DbConn, + ) -> EmptyResult { + // Update the grantee so that it will refresh it's status. + User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await; + self.status = status; + self.updated_at = date.to_owned(); + + db_run! {conn: { + crate::util::retry(|| { + diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) + .set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date))) + .execute(conn) + }, 10) + .map_res("Error updating emergency access status") + }} + } + + pub async fn update_last_notification_date_and_save( + &mut self, + date: &NaiveDateTime, + conn: &mut DbConn, + ) -> EmptyResult { + self.last_notification_at = Some(date.to_owned()); + self.updated_at = date.to_owned(); + + db_run! {conn: { + crate::util::retry(|| { + diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) + .set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date))) + .execute(conn) + }, 10) + .map_res("Error updating emergency access status") + }} + } + pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await { ea.delete(conn).await?; @@ -233,10 +248,11 @@ impl EmergencyAccess { }} } - pub async fn find_all_recoveries(conn: &mut DbConn) -> Vec { + pub async fn find_all_recoveries_initiated(conn: &mut DbConn) -> Vec { db_run! { conn: { emergency_access::table .filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32)) + .filter(emergency_access::recovery_initiated_at.is_not_null()) .load::(conn).expect("Error loading emergency_access").from_db() }} } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 68fb96f6..b59f10b8 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -364,7 +364,7 @@ impl User { } impl Invitation { - pub fn new(email: String) -> Self { + pub fn new(email: &str) -> Self { let email = email.to_lowercase(); Self { email, diff --git a/src/error.rs b/src/error.rs index decae01e..582604fc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -168,7 +168,6 @@ impl MapResult for Option { } } -#[allow(clippy::unnecessary_wraps)] const fn _has_source(e: T) -> Option { Some(e) } diff --git a/src/mail.rs b/src/mail.rs index fce76e17..af0f8c7c 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -256,16 +256,16 @@ pub async fn send_invite( pub async fn send_emergency_access_invite( address: &str, uuid: &str, - emer_id: Option, - grantor_name: Option, - grantor_email: Option, + emer_id: &str, + grantor_name: &str, + grantor_email: &str, ) -> EmptyResult { let claims = generate_emergency_access_invite_claims( - uuid.to_string(), + String::from(uuid), String::from(address), - emer_id.clone(), - grantor_name.clone(), - grantor_email, + String::from(emer_id), + String::from(grantor_name), + String::from(grantor_email), ); let invite_token = encode_jwt(&claims); @@ -275,7 +275,7 @@ pub async fn send_emergency_access_invite( json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), - "emer_id": emer_id.unwrap_or_else(|| "_".to_string()), + "emer_id": emer_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "grantor_name": grantor_name, "token": invite_token, @@ -328,7 +328,7 @@ pub async fn send_emergency_access_recovery_initiated( address: &str, grantee_name: &str, atype: &str, - wait_time_days: &str, + wait_time_days: &i32, ) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_initiated", From 7b09d74b1ff9c27b70663eda847895d24871492d Mon Sep 17 00:00:00 2001 From: BlackDex Date: Thu, 1 Dec 2022 17:18:29 +0100 Subject: [PATCH 3/5] Update dependencies for Rust and Admin interface. - Updated Rust deps and one small change regarding chrono - Updated bootstrap 5 css - Updated datatables - Replaced identicon.js with jdenticon. identicon.js is unmaintained ( https://github.com/stewartlord/identicon.js/issues/52 ) The icon's are very different, but nice. It also doesn't need custom code to find and update the icons our selfs. --- Cargo.lock | 205 +-- Cargo.toml | 24 +- src/api/web.rs | 2 +- src/static/scripts/bootstrap.css | 199 +-- src/static/scripts/datatables.css | 16 +- src/static/scripts/datatables.js | 117 +- src/static/scripts/identicon.js | 205 --- src/static/scripts/jdenticon.js | 1462 ++++++++++++++++++ src/static/templates/admin/base.hbs | 7 +- src/static/templates/admin/organizations.hbs | 8 +- src/static/templates/admin/users.hbs | 8 +- 11 files changed, 1756 insertions(+), 497 deletions(-) delete mode 100644 src/static/scripts/identicon.js create mode 100644 src/static/scripts/jdenticon.js diff --git a/Cargo.lock b/Cargo.lock index c402442d..699df060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -173,7 +173,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.5.4", "object", "rustc-demangle", ] @@ -240,9 +240,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cached" @@ -283,9 +283,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" [[package]] name = "cc" -version = "1.0.75" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ca34107f97baef6cfb231b32f36115781856b8f8208e8c580e0bcaea374842" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -295,9 +295,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "num-integer", @@ -308,9 +308,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87b30366b6766751277791b473b674f3bf7fb75696841c784a3eb7e7fbf44ee" +checksum = "fa48fa079165080f11d7753fd0bc175b7d391f276b965fe4b55bfad67856e463" dependencies = [ "chrono", "chrono-tz-build", @@ -445,9 +445,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", ] @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" dependencies = [ "cc", "cxxbridge-flags", @@ -496,9 +496,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" dependencies = [ "cc", "codespan-reporting", @@ -511,15 +511,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" [[package]] name = "cxxbridge-macro" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ "proc-macro2", "quote", @@ -673,9 +673,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -724,9 +724,9 @@ dependencies = [ [[package]] name = "enum-as-inner" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck", "proc-macro2", @@ -778,12 +778,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.6.2", ] [[package]] @@ -969,9 +969,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "governor" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de1b4626e87b9eb1d603ed23067ba1e29ec1d0b35325a2b96c3fe1cf20871f56" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" dependencies = [ "cfg-if", "dashmap", @@ -1208,9 +1208,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -1243,14 +1243,14 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" dependencies = [ "socket2", "widestring", "winapi", - "winreg 0.7.0", + "winreg", ] [[package]] @@ -1293,9 +1293,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "8.1.1" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c" +checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828" dependencies = [ "base64", "pem", @@ -1341,17 +1341,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libmimalloc-sys" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c37567b180c1af25924b303ddf1ee4467653783440c62360beb2b322a4d93361" +checksum = "04d1c67deb83e6b75fa4fe3309e09cfeade12e7721d95322af500d3814ea60c9" dependencies = [ "cc", + "libc", ] [[package]] @@ -1482,9 +1483,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32d6a9ac92d0239d7bfa31137fb47634ac7272a3c11bcee91379ac100781670" +checksum = "9b2374e2999959a7b583e1811a1ddbf1d3a4b9496eceb9746f1192a59d871eca" dependencies = [ "libmimalloc-sys", ] @@ -1510,6 +1511,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.5" @@ -1571,9 +1581,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", "bitflags", @@ -1696,9 +1706,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.42" +version = "0.10.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" dependencies = [ "bitflags", "cfg-if", @@ -1737,9 +1747,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.77" +version = "0.9.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" +checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132" dependencies = [ "autocfg", "cc", @@ -1767,9 +1777,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if", "libc", @@ -1833,9 +1843,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" dependencies = [ "thiserror", "ucd-trie", @@ -1843,9 +1853,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fd9bc6500181952d34bd0b2b0163a54d794227b498be0b7afa7698d0a7b18f" +checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344" dependencies = [ "pest", "pest_generator", @@ -1853,9 +1863,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2610d5ac5156217b4ff8e46ddcef7cdf44b273da2ac5bca2ecbfa86a330e7c4" +checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c" dependencies = [ "pest", "pest_meta", @@ -1866,9 +1876,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824749bf7e21dd66b36fbe26b3f45c713879cccd4a009a917ab8e045ca8246fe" +checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20" dependencies = [ "once_cell", "pest", @@ -2065,9 +2075,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" [[package]] name = "r2d2" @@ -2185,9 +2195,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ "async-compression", "base64", @@ -2224,7 +2234,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.10.1", + "winreg", ] [[package]] @@ -2484,9 +2494,9 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" dependencies = [ "serde_derive", ] @@ -2503,9 +2513,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" dependencies = [ "proc-macro2", "quote", @@ -2514,9 +2524,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -2537,9 +2547,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", @@ -2673,9 +2683,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -2802,9 +2812,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg", "bytes", @@ -2822,9 +2832,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -2877,9 +2887,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.17.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" dependencies = [ "futures-util", "log", @@ -2993,9 +3003,9 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" dependencies = [ "async-trait", "cfg-if", @@ -3007,32 +3017,32 @@ dependencies = [ "idna 0.2.3", "ipnet", "lazy_static", - "log", "rand", "smallvec", "thiserror", "tinyvec", "tokio", + "tracing", "url", ] [[package]] name = "trust-dns-resolver" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" dependencies = [ "cfg-if", "futures-util", "ipconfig", "lazy_static", - "log", "lru-cache", "parking_lot", "resolv-conf", "smallvec", "thiserror", "tokio", + "tracing", "trust-dns-proto", ] @@ -3044,9 +3054,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" dependencies = [ "base64", "byteorder", @@ -3055,7 +3065,7 @@ dependencies = [ "httparse", "log", "rand", - "sha-1", + "sha1", "thiserror", "url", "utf-8", @@ -3161,9 +3171,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom", ] @@ -3563,15 +3573,6 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" -[[package]] -name = "winreg" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index c667ab90..c5fc70db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,17 +60,17 @@ num-derive = "0.3.3" rocket = { version = "0.5.0-rc.2", features = ["tls", "json"], default-features = false } # WebSockets libraries -tokio-tungstenite = "0.17.2" +tokio-tungstenite = "0.18.0" rmpv = "1.0.0" # MessagePack library dashmap = "5.4.0" # Async futures futures = "0.3.25" -tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time"] } +tokio = { version = "1.22.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time"] } # A generic serialization/deserialization framework -serde = { version = "1.0.147", features = ["derive"] } -serde_json = "1.0.87" +serde = { version = "1.0.148", features = ["derive"] } +serde_json = "1.0.89" # A safe, extensible ORM and Query builder diesel = { version = "2.0.2", features = ["chrono", "r2d2"] } @@ -85,11 +85,11 @@ rand = { version = "0.8.5", features = ["small_rng"] } ring = "0.16.20" # UUID generation -uuid = { version = "1.2.1", features = ["v4"] } +uuid = { version = "1.2.2", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.22", features = ["clock", "serde"], default-features = false } -chrono-tz = "0.8.0" +chrono = { version = "0.4.23", features = ["clock", "serde"], default-features = false } +chrono-tz = "0.8.1" time = "0.3.17" # Job scheduler @@ -122,13 +122,13 @@ email_address = "0.2.4" handlebars = { version = "4.3.5", features = ["dir_source"] } # HTTP client -reqwest = { version = "0.11.12", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] } +reqwest = { version = "0.11.13", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] } # For favicon extraction from main website html5gum = "0.5.2" regex = { version = "1.7.0", features = ["std", "perf", "unicode-perl"], default-features = false } data-url = "0.2.0" -bytes = "1.2.1" +bytes = "1.3.0" cached = "0.40.0" # Used for custom short lived cookie jar during favicon extraction @@ -136,14 +136,14 @@ cookie = "0.16.1" cookie_store = "0.19.0" # Used by U2F, JWT and Postgres -openssl = "0.10.42" +openssl = "0.10.43" # CLI argument parsing pico-args = "0.5.0" # Macro ident concatenation paste = "1.0.9" -governor = "0.5.0" +governor = "0.5.1" # Capture CTRL+C ctrlc = { version = "3.2.3", features = ["termination"] } @@ -153,7 +153,7 @@ semver = "1.0.14" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow -mimalloc = { version = "0.1.31", features = ["secure"], default-features = false, optional = true } +mimalloc = { version = "0.1.32", features = ["secure"], default-features = false, optional = true } [patch.crates-io] # Using a patched version of multer-rs (Used by Rocket) to fix attachment/send file uploads diff --git a/src/api/web.rs b/src/api/web.rs index cfc4b9e0..e5485123 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -98,7 +98,7 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er "vaultwarden-icon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-icon.png"))), "bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))), "bootstrap-native.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))), - "identicon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))), + "jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))), "datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))), "datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))), "jquery-3.6.1.slim.js" => { diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css index 3e4ae582..fa2da29b 100644 --- a/src/static/scripts/bootstrap.css +++ b/src/static/scripts/bootstrap.css @@ -1,6 +1,6 @@ @charset "UTF-8"; /*! - * Bootstrap v5.2.0 (https://getbootstrap.com/) + * Bootstrap v5.2.3 (https://getbootstrap.com/) * Copyright 2011-2022 The Bootstrap Authors * Copyright 2011-2022 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) @@ -2468,6 +2468,7 @@ textarea.form-control-lg { height: 100%; padding: 1rem 0.75rem; overflow: hidden; + text-align: start; text-overflow: ellipsis; white-space: nowrap; pointer-events: none; @@ -2547,14 +2548,14 @@ textarea.form-control-lg { .input-group > .form-control:focus, .input-group > .form-select:focus, .input-group > .form-floating:focus-within { - z-index: 3; + z-index: 5; } .input-group .btn { position: relative; z-index: 2; } .input-group .btn:focus { - z-index: 3; + z-index: 5; } .input-group-text { @@ -2609,10 +2610,13 @@ textarea.form-control-lg { border-top-right-radius: 0; border-bottom-right-radius: 0; } -.input-group > :not(:first-child):not(.dropdown-menu):not(.form-floating):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback), +.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} .input-group > .form-floating:not(:first-child) > .form-control, .input-group > .form-floating:not(:first-child) > .form-select { - margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -2699,14 +2703,11 @@ textarea.form-control-lg { margin-left: 0.5em; } -.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, -.was-validated .input-group .form-select:valid, -.input-group .form-select.is-valid { - z-index: 1; -} -.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, -.was-validated .input-group .form-select:valid:focus, -.input-group .form-select.is-valid:focus { +.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, +.was-validated .input-group > .form-select:not(:focus):valid, +.input-group > .form-select:not(:focus).is-valid, +.was-validated .input-group > .form-floating:not(:focus-within):valid, +.input-group > .form-floating:not(:focus-within).is-valid { z-index: 3; } @@ -2792,15 +2793,12 @@ textarea.form-control-lg { margin-left: 0.5em; } -.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, -.was-validated .input-group .form-select:invalid, -.input-group .form-select.is-invalid { - z-index: 2; -} -.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, -.was-validated .input-group .form-select:invalid:focus, -.input-group .form-select.is-invalid:focus { - z-index: 3; +.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, +.was-validated .input-group > .form-select:not(:focus):invalid, +.input-group > .form-select:not(:focus).is-invalid, +.was-validated .input-group > .form-floating:not(:focus-within):invalid, +.input-group > .form-floating:not(:focus-within).is-invalid { + z-index: 4; } .btn { @@ -2815,6 +2813,7 @@ textarea.form-control-lg { --bs-btn-border-width: 1px; --bs-btn-border-color: transparent; --bs-btn-border-radius: 0.375rem; + --bs-btn-hover-border-color: transparent; --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); --bs-btn-disabled-opacity: 0.65; --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5); @@ -2847,19 +2846,29 @@ textarea.form-control-lg { background-color: var(--bs-btn-hover-bg); border-color: var(--bs-btn-hover-border-color); } -.btn-check:focus + .btn, .btn:focus { +.btn-check + .btn:hover { + color: var(--bs-btn-color); + background-color: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); +} +.btn:focus-visible { color: var(--bs-btn-hover-color); background-color: var(--bs-btn-hover-bg); border-color: var(--bs-btn-hover-border-color); outline: 0; box-shadow: var(--bs-btn-focus-box-shadow); } -.btn-check:checked + .btn, .btn-check:active + .btn, .btn:active, .btn.active, .btn.show { +.btn-check:focus-visible + .btn { + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { color: var(--bs-btn-active-color); background-color: var(--bs-btn-active-bg); border-color: var(--bs-btn-active-border-color); } -.btn-check:checked + .btn:focus, .btn-check:active + .btn:focus, .btn:active:focus, .btn.active:focus, .btn.show:focus { +.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { box-shadow: var(--bs-btn-focus-box-shadow); } .btn:disabled, .btn.disabled, fieldset:disabled .btn { @@ -3157,7 +3166,7 @@ textarea.form-control-lg { --bs-btn-focus-shadow-rgb: 49, 132, 253; text-decoration: underline; } -.btn-link:focus { +.btn-link:focus-visible { color: var(--bs-btn-color); } .btn-link:hover { @@ -3242,6 +3251,7 @@ textarea.form-control-lg { } .dropdown-menu { + --bs-dropdown-zindex: 1000; --bs-dropdown-min-width: 10rem; --bs-dropdown-padding-x: 0; --bs-dropdown-padding-y: 0.5rem; @@ -3268,7 +3278,7 @@ textarea.form-control-lg { --bs-dropdown-header-padding-x: 1rem; --bs-dropdown-header-padding-y: 0.5rem; position: absolute; - z-index: 1000; + z-index: var(--bs-dropdown-zindex); display: none; min-width: var(--bs-dropdown-min-width); padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); @@ -3568,7 +3578,7 @@ textarea.form-control-lg { .btn-group { border-radius: 0.375rem; } -.btn-group > .btn:not(:first-child), +.btn-group > :not(.btn-check:first-child) + .btn, .btn-group > .btn-group:not(:first-child) { margin-left: -1px; } @@ -3678,7 +3688,7 @@ textarea.form-control-lg { border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); } .nav-tabs .nav-link { - margin-bottom: calc(var(--bs-nav-tabs-border-width) * -1); + margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width)); background: none; border: var(--bs-nav-tabs-border-width) solid transparent; border-top-left-radius: var(--bs-nav-tabs-border-radius); @@ -3700,7 +3710,7 @@ textarea.form-control-lg { border-color: var(--bs-nav-tabs-link-active-border-color); } .nav-tabs .dropdown-menu { - margin-top: calc(var(--bs-nav-tabs-border-width) * -1); + margin-top: calc(-1 * var(--bs-nav-tabs-border-width)); border-top-left-radius: 0; border-top-right-radius: 0; } @@ -4357,7 +4367,7 @@ textarea.form-control-lg { } .accordion { - --bs-accordion-color: #000; + --bs-accordion-color: #212529; --bs-accordion-bg: #fff; --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; --bs-accordion-border-color: var(--bs-border-color); @@ -4366,9 +4376,9 @@ textarea.form-control-lg { --bs-accordion-inner-border-radius: calc(0.375rem - 1px); --bs-accordion-btn-padding-x: 1.25rem; --bs-accordion-btn-padding-y: 1rem; - --bs-accordion-btn-color: var(--bs-body-color); + --bs-accordion-btn-color: #212529; --bs-accordion-btn-bg: var(--bs-accordion-bg); - --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-body-color%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); --bs-accordion-btn-icon-width: 1.25rem; --bs-accordion-btn-icon-transform: rotate(-180deg); --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; @@ -4404,7 +4414,7 @@ textarea.form-control-lg { .accordion-button:not(.collapsed) { color: var(--bs-accordion-active-color); background-color: var(--bs-accordion-active-bg); - box-shadow: inset 0 calc(var(--bs-accordion-border-width) * -1) 0 var(--bs-accordion-border-color); + box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); } .accordion-button:not(.collapsed)::after { background-image: var(--bs-accordion-btn-active-icon); @@ -4487,7 +4497,7 @@ textarea.form-control-lg { .accordion-flush .accordion-item:last-child { border-bottom: 0; } -.accordion-flush .accordion-item .accordion-button { +.accordion-flush .accordion-item .accordion-button, .accordion-flush .accordion-item .accordion-button.collapsed { border-radius: 0; } @@ -4753,12 +4763,6 @@ textarea.form-control-lg { color: #101214; } -@-webkit-keyframes progress-bar-stripes { - 0% { - background-position-x: 1rem; - } -} - @keyframes progress-bar-stripes { 0% { background-position-x: 1rem; @@ -4804,12 +4808,10 @@ textarea.form-control-lg { } .progress-bar-animated { - -webkit-animation: 1s linear infinite progress-bar-stripes; animation: 1s linear infinite progress-bar-stripes; } @media (prefers-reduced-motion: reduce) { .progress-bar-animated { - -webkit-animation: none; animation: none; } } @@ -4896,18 +4898,18 @@ textarea.form-control-lg { border-top-width: 0; } .list-group-item + .list-group-item.active { - margin-top: calc(var(--bs-list-group-border-width) * -1); + margin-top: calc(-1 * var(--bs-list-group-border-width)); border-top-width: var(--bs-list-group-border-width); } .list-group-horizontal { flex-direction: row; } -.list-group-horizontal > .list-group-item:first-child { +.list-group-horizontal > .list-group-item:first-child:not(:last-child) { border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } -.list-group-horizontal > .list-group-item:last-child { +.list-group-horizontal > .list-group-item:last-child:not(:first-child) { border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } @@ -4919,7 +4921,7 @@ textarea.form-control-lg { border-left-width: 0; } .list-group-horizontal > .list-group-item + .list-group-item.active { - margin-left: calc(var(--bs-list-group-border-width) * -1); + margin-left: calc(-1 * var(--bs-list-group-border-width)); border-left-width: var(--bs-list-group-border-width); } @@ -4927,11 +4929,11 @@ textarea.form-control-lg { .list-group-horizontal-sm { flex-direction: row; } - .list-group-horizontal-sm > .list-group-item:first-child { + .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) { border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-sm > .list-group-item:last-child { + .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) { border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } @@ -4943,7 +4945,7 @@ textarea.form-control-lg { border-left-width: 0; } .list-group-horizontal-sm > .list-group-item + .list-group-item.active { - margin-left: calc(var(--bs-list-group-border-width) * -1); + margin-left: calc(-1 * var(--bs-list-group-border-width)); border-left-width: var(--bs-list-group-border-width); } } @@ -4951,11 +4953,11 @@ textarea.form-control-lg { .list-group-horizontal-md { flex-direction: row; } - .list-group-horizontal-md > .list-group-item:first-child { + .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) { border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-md > .list-group-item:last-child { + .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) { border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } @@ -4967,7 +4969,7 @@ textarea.form-control-lg { border-left-width: 0; } .list-group-horizontal-md > .list-group-item + .list-group-item.active { - margin-left: calc(var(--bs-list-group-border-width) * -1); + margin-left: calc(-1 * var(--bs-list-group-border-width)); border-left-width: var(--bs-list-group-border-width); } } @@ -4975,11 +4977,11 @@ textarea.form-control-lg { .list-group-horizontal-lg { flex-direction: row; } - .list-group-horizontal-lg > .list-group-item:first-child { + .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) { border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-lg > .list-group-item:last-child { + .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) { border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } @@ -4991,7 +4993,7 @@ textarea.form-control-lg { border-left-width: 0; } .list-group-horizontal-lg > .list-group-item + .list-group-item.active { - margin-left: calc(var(--bs-list-group-border-width) * -1); + margin-left: calc(-1 * var(--bs-list-group-border-width)); border-left-width: var(--bs-list-group-border-width); } } @@ -4999,11 +5001,11 @@ textarea.form-control-lg { .list-group-horizontal-xl { flex-direction: row; } - .list-group-horizontal-xl > .list-group-item:first-child { + .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) { border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-xl > .list-group-item:last-child { + .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) { border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } @@ -5015,7 +5017,7 @@ textarea.form-control-lg { border-left-width: 0; } .list-group-horizontal-xl > .list-group-item + .list-group-item.active { - margin-left: calc(var(--bs-list-group-border-width) * -1); + margin-left: calc(-1 * var(--bs-list-group-border-width)); border-left-width: var(--bs-list-group-border-width); } } @@ -5023,11 +5025,11 @@ textarea.form-control-lg { .list-group-horizontal-xxl { flex-direction: row; } - .list-group-horizontal-xxl > .list-group-item:first-child { + .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) { border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-xxl > .list-group-item:last-child { + .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) { border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } @@ -5039,7 +5041,7 @@ textarea.form-control-lg { border-left-width: 0; } .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { - margin-left: calc(var(--bs-list-group-border-width) * -1); + margin-left: calc(-1 * var(--bs-list-group-border-width)); border-left-width: var(--bs-list-group-border-width); } } @@ -5199,6 +5201,7 @@ textarea.form-control-lg { } .toast { + --bs-toast-zindex: 1090; --bs-toast-padding-x: 0.75rem; --bs-toast-padding-y: 0.5rem; --bs-toast-spacing: 1.5rem; @@ -5232,8 +5235,9 @@ textarea.form-control-lg { } .toast-container { + --bs-toast-zindex: 1090; position: absolute; - z-index: 1090; + z-index: var(--bs-toast-zindex); width: -webkit-max-content; width: -moz-max-content; width: max-content; @@ -5256,7 +5260,7 @@ textarea.form-control-lg { border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); } .toast-header .btn-close { - margin-right: calc(var(--bs-toast-padding-x) * -0.5); + margin-right: calc(-0.5 * var(--bs-toast-padding-x)); margin-left: var(--bs-toast-padding-x); } @@ -5383,7 +5387,7 @@ textarea.form-control-lg { } .modal-header .btn-close { padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5); - margin: calc(var(--bs-modal-header-padding-y) * -0.5) calc(var(--bs-modal-header-padding-x) * -0.5) calc(var(--bs-modal-header-padding-y) * -0.5) auto; + margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto; } .modal-title { @@ -5673,7 +5677,7 @@ textarea.form-control-lg { --bs-popover-header-padding-x: 1rem; --bs-popover-header-padding-y: 0.5rem; --bs-popover-header-font-size: 1rem; - --bs-popover-header-color: var(--bs-heading-color); + --bs-popover-header-color: ; --bs-popover-header-bg: #f0f0f0; --bs-popover-body-padding-x: 1rem; --bs-popover-body-padding-y: 1rem; @@ -5720,7 +5724,7 @@ textarea.form-control-lg { } .bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { - bottom: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width)); + bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); } .bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; @@ -5736,7 +5740,7 @@ textarea.form-control-lg { /* rtl:begin:ignore */ .bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { - left: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width)); + left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); width: var(--bs-popover-arrow-height); height: var(--bs-popover-arrow-width); } @@ -5754,7 +5758,7 @@ textarea.form-control-lg { /* rtl:end:ignore */ .bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { - top: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width)); + top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); } .bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); @@ -5773,14 +5777,14 @@ textarea.form-control-lg { left: 50%; display: block; width: var(--bs-popover-arrow-width); - margin-left: calc(var(--bs-popover-arrow-width) * -0.5); + margin-left: calc(-0.5 * var(--bs-popover-arrow-width)); content: ""; border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); } /* rtl:begin:ignore */ .bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { - right: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width)); + right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); width: var(--bs-popover-arrow-height); height: var(--bs-popover-arrow-width); } @@ -5857,7 +5861,6 @@ textarea.form-control-lg { display: block; } -/* rtl:begin:ignore */ .carousel-item-next:not(.carousel-item-start), .active.carousel-item-end { transform: translateX(100%); @@ -5868,7 +5871,6 @@ textarea.form-control-lg { transform: translateX(-100%); } -/* rtl:end:ignore */ .carousel-fade .carousel-item { opacity: 0; transition-property: opacity; @@ -6030,16 +6032,9 @@ textarea.form-control-lg { height: var(--bs-spinner-height); vertical-align: var(--bs-spinner-vertical-align); border-radius: 50%; - -webkit-animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); } -@-webkit-keyframes spinner-border { - to { - transform: rotate(360deg) /* rtl:ignore */; - } -} - @keyframes spinner-border { to { transform: rotate(360deg) /* rtl:ignore */; @@ -6062,16 +6057,6 @@ textarea.form-control-lg { --bs-spinner-border-width: 0.2em; } -@-webkit-keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } -} - @keyframes spinner-grow { 0% { transform: scale(0); @@ -6103,6 +6088,7 @@ textarea.form-control-lg { } } .offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm { + --bs-offcanvas-zindex: 1045; --bs-offcanvas-width: 400px; --bs-offcanvas-height: 30vh; --bs-offcanvas-padding-x: 1rem; @@ -6118,7 +6104,7 @@ textarea.form-control-lg { .offcanvas-sm { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; @@ -6206,7 +6192,7 @@ textarea.form-control-lg { .offcanvas-md { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; @@ -6294,7 +6280,7 @@ textarea.form-control-lg { .offcanvas-lg { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; @@ -6382,7 +6368,7 @@ textarea.form-control-lg { .offcanvas-xl { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; @@ -6470,7 +6456,7 @@ textarea.form-control-lg { .offcanvas-xxl { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; @@ -6557,7 +6543,7 @@ textarea.form-control-lg { .offcanvas { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; @@ -6635,9 +6621,9 @@ textarea.form-control-lg { } .offcanvas-header .btn-close { padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); - margin-top: calc(var(--bs-offcanvas-padding-y) * -0.5); - margin-right: calc(var(--bs-offcanvas-padding-x) * -0.5); - margin-bottom: calc(var(--bs-offcanvas-padding-y) * -0.5); + margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y)); } .offcanvas-title { @@ -6677,16 +6663,9 @@ textarea.form-control-lg { } .placeholder-glow .placeholder { - -webkit-animation: placeholder-glow 2s ease-in-out infinite; animation: placeholder-glow 2s ease-in-out infinite; } -@-webkit-keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} - @keyframes placeholder-glow { 50% { opacity: 0.2; @@ -6697,17 +6676,9 @@ textarea.form-control-lg { mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); -webkit-mask-size: 200% 100%; mask-size: 200% 100%; - -webkit-animation: placeholder-wave 2s linear infinite; animation: placeholder-wave 2s linear infinite; } -@-webkit-keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} - @keyframes placeholder-wave { 100% { -webkit-mask-position: -200% 0%; diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index bc9eb81c..a19cb110 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.12.1 + * https://datatables.net/download/#bs5/dt-1.13.1 * * Included libraries: - * DataTables 1.12.1 + * DataTables 1.13.1 */ @charset "UTF-8"; @@ -63,7 +63,7 @@ table.dataTable thead > tr > td.sorting_desc_disabled:after { opacity: 0.125; right: 10px; line-height: 9px; - font-size: 0.9em; + font-size: 0.8em; } table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > td.sorting:before, @@ -72,7 +72,7 @@ table.dataTable thead > tr > td.sorting_desc:before, table.dataTable thead > tr > td.sorting_asc_disabled:before, table.dataTable thead > tr > td.sorting_desc_disabled:before { bottom: 50%; - content: "▴"; + content: "▲"; } table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > td.sorting:after, @@ -81,7 +81,7 @@ table.dataTable thead > tr > td.sorting_desc:after, table.dataTable thead > tr > td.sorting_asc_disabled:after, table.dataTable thead > tr > td.sorting_desc_disabled:after { top: 50%; - content: "▾"; + content: "▼"; } table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > td.sorting_asc:before, @@ -287,6 +287,9 @@ table.dataTable > tbody > tr.selected > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9); color: white; } +table.dataTable > tbody > tr.selected a { + color: #090a0b; +} table.dataTable.table-striped > tbody > tr.odd > * { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); } @@ -335,6 +338,9 @@ div.dataTables_wrapper div.dataTables_paginate ul.pagination { white-space: nowrap; justify-content: flex-end; } +div.dataTables_wrapper div.dt-row { + position: relative; +} div.dataTables_scrollHead table.dataTable { margin-bottom: 0 !important; diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 9b1448ad..1aeda982 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/#bs5/dt-1.12.1 + * https://datatables.net/download/#bs5/dt-1.13.1 * * Included libraries: - * DataTables 1.12.1 + * DataTables 1.13.1 */ -/*! DataTables 1.12.1 +/*! DataTables 1.13.1 * ©2008-2022 SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.12.1 + * @version 1.13.1 * @author SpryMedia Ltd * @contact www.datatables.net * @copyright SpryMedia Ltd. @@ -1162,6 +1162,10 @@ $( rowOne[0] ).children('th, td').each( function (i, cell) { var col = oSettings.aoColumns[i]; + if (! col) { + _fnLog( oSettings, 0, 'Incorrect column count', 18 ); + } + if ( col.mData === i ) { var sort = a( cell, 'sort' ) || a( cell, 'order' ); var filter = a( cell, 'filter' ) || a( cell, 'search' ); @@ -3166,6 +3170,11 @@ create = nTrIn ? false : true; nTd = create ? document.createElement( oCol.sCellType ) : anTds[i]; + + if (! nTd) { + _fnLog( oSettings, 0, 'Incorrect column count', 18 ); + } + nTd._DT_CellIndex = { row: iRow, column: i @@ -3316,10 +3325,16 @@ for ( i=0, ien=cells.length ; i<'col-sm-12 col-md-6'f>>" + - "<'row'<'col-sm-12'tr>>" + + "<'row dt-row'<'col-sm-12'tr>>" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", renderer: 'bootstrap' } ); @@ -15663,7 +15696,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu var classes = settings.oClasses; var lang = settings.oLanguage.oPaginate; var aria = settings.oLanguage.oAria.paginate || {}; - var btnDisplay, btnClass, counter=0; + var btnDisplay, btnClass; var attach = function( container, buttons ) { var i, ien, node, button; @@ -15732,7 +15765,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu 'href': '#', 'aria-controls': settings.sTableId, 'aria-label': aria[ button ], - 'data-dt-idx': counter, + 'data-dt-idx': button, 'tabindex': settings.iTabIndex, 'class': 'page-link' } ) @@ -15743,13 +15776,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu settings.oApi._fnBindAction( node, {action: button}, clickHandler ); - - counter++; } } } }; + var hostEl = $(host); // IE9 throws an 'unknown error' if document.activeElement is used // inside an iframe or frame. var activeEl; @@ -15759,17 +15791,26 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu // elements, focus is lost on the select button which is bad for // accessibility. So we want to restore focus once the draw has // completed - activeEl = $(host).find(document.activeElement).data('dt-idx'); + activeEl = hostEl.find(document.activeElement).data('dt-idx'); } catch (e) {} + var paginationEl = hostEl.children('ul.pagination'); + + if (paginationEl.length) { + paginationEl.empty(); + } + else { + paginationEl = hostEl.html('
    ').children('ul').addClass('pagination'); + } + attach( - $(host).empty().html('
      ').children('ul'), + paginationEl, buttons ); if ( activeEl !== undefined ) { - $(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus'); + hostEl.find('[data-dt-idx='+activeEl+']').trigger('focus'); } }; diff --git a/src/static/scripts/identicon.js b/src/static/scripts/identicon.js deleted file mode 100644 index cd351cce..00000000 --- a/src/static/scripts/identicon.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Identicon.js 2.3.3 - * http://github.com/stewartlord/identicon.js - * - * PNGLib required for PNG output - * http://www.xarg.org/download/pnglib.js - * - * Copyright 2018, Stewart Lord - * Released under the BSD license - * http://www.opensource.org/licenses/bsd-license.php - */ - -(function() { - var PNGlib; - if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - PNGlib = require('./pnglib'); - } else { - PNGlib = window.PNGlib; - } - - var Identicon = function(hash, options){ - if (typeof(hash) !== 'string' || hash.length < 15) { - throw 'A hash of at least 15 characters is required.'; - } - - this.defaults = { - background: [240, 240, 240, 255], - margin: 0.08, - size: 64, - saturation: 0.7, - brightness: 0.5, - format: 'png' - }; - - this.options = typeof(options) === 'object' ? options : this.defaults; - - // backward compatibility with old constructor (hash, size, margin) - if (typeof(arguments[1]) === 'number') { this.options.size = arguments[1]; } - if (arguments[2]) { this.options.margin = arguments[2]; } - - this.hash = hash - this.background = this.options.background || this.defaults.background; - this.size = this.options.size || this.defaults.size; - this.format = this.options.format || this.defaults.format; - this.margin = this.options.margin !== undefined ? this.options.margin : this.defaults.margin; - - // foreground defaults to last 7 chars as hue at 70% saturation, 50% brightness - var hue = parseInt(this.hash.substr(-7), 16) / 0xfffffff; - var saturation = this.options.saturation || this.defaults.saturation; - var brightness = this.options.brightness || this.defaults.brightness; - this.foreground = this.options.foreground || this.hsl2rgb(hue, saturation, brightness); - }; - - Identicon.prototype = { - background: null, - foreground: null, - hash: null, - margin: null, - size: null, - format: null, - - image: function(){ - return this.isSvg() - ? new Svg(this.size, this.foreground, this.background) - : new PNGlib(this.size, this.size, 256); - }, - - render: function(){ - var image = this.image(), - size = this.size, - baseMargin = Math.floor(size * this.margin), - cell = Math.floor((size - (baseMargin * 2)) / 5), - margin = Math.floor((size - cell * 5) / 2), - bg = image.color.apply(image, this.background), - fg = image.color.apply(image, this.foreground); - - // the first 15 characters of the hash control the pixels (even/odd) - // they are drawn down the middle first, then mirrored outwards - var i, color; - for (i = 0; i < 15; i++) { - color = parseInt(this.hash.charAt(i), 16) % 2 ? bg : fg; - if (i < 5) { - this.rectangle(2 * cell + margin, i * cell + margin, cell, cell, color, image); - } else if (i < 10) { - this.rectangle(1 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image); - this.rectangle(3 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image); - } else if (i < 15) { - this.rectangle(0 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image); - this.rectangle(4 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image); - } - } - - return image; - }, - - rectangle: function(x, y, w, h, color, image){ - if (this.isSvg()) { - image.rectangles.push({x: x, y: y, w: w, h: h, color: color}); - } else { - var i, j; - for (i = x; i < x + w; i++) { - for (j = y; j < y + h; j++) { - image.buffer[image.index(i, j)] = color; - } - } - } - }, - - // adapted from: https://gist.github.com/aemkei/1325937 - hsl2rgb: function(h, s, b){ - h *= 6; - s = [ - b += s *= b < .5 ? b : 1 - b, - b - h % 1 * s * 2, - b -= s *= 2, - b, - b + h % 1 * s, - b + s - ]; - - return[ - s[ ~~h % 6 ] * 255, // red - s[ (h|16) % 6 ] * 255, // green - s[ (h|8) % 6 ] * 255 // blue - ]; - }, - - toString: function(raw){ - // backward compatibility with old toString, default to base64 - if (raw) { - return this.render().getDump(); - } else { - return this.render().getBase64(); - } - }, - - isSvg: function(){ - return this.format.match(/svg/i) - } - }; - - var Svg = function(size, foreground, background){ - this.size = size; - this.foreground = this.color.apply(this, foreground); - this.background = this.color.apply(this, background); - this.rectangles = []; - }; - - Svg.prototype = { - size: null, - foreground: null, - background: null, - rectangles: null, - - color: function(r, g, b, a){ - var values = [r, g, b].map(Math.round); - values.push((a >= 0) && (a <= 255) ? a/255 : 1); - return 'rgba(' + values.join(',') + ')'; - }, - - getDump: function(){ - var i, - xml, - rect, - fg = this.foreground, - bg = this.background, - stroke = this.size * 0.005; - - xml = "" - + ""; - - for (i = 0; i < this.rectangles.length; i++) { - rect = this.rectangles[i]; - if (rect.color == bg) continue; - xml += ""; - } - xml += "" - - return xml; - }, - - getBase64: function(){ - if ('function' === typeof btoa) { - return btoa(this.getDump()); - } else if (Buffer) { - return new Buffer(this.getDump(), 'binary').toString('base64'); - } else { - throw 'Cannot generate base64 output'; - } - } - }; - - if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = Identicon; - } else { - window.Identicon = Identicon; - } -})(); diff --git a/src/static/scripts/jdenticon.js b/src/static/scripts/jdenticon.js new file mode 100644 index 00000000..1144d379 --- /dev/null +++ b/src/static/scripts/jdenticon.js @@ -0,0 +1,1462 @@ +/** + * Jdenticon 3.2.0 + * http://jdenticon.com + * + * Built: 2022-08-07T11:23:11.640Z + * + * MIT License + * + * Copyright (c) 2014-2021 Daniel Mester Pirttijärvi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +(function (umdGlobal, factory) { + var jdenticon = factory(umdGlobal); + + // Node.js + if (typeof module !== "undefined" && "exports" in module) { + module["exports"] = jdenticon; + } + // RequireJS + else if (typeof define === "function" && define["amd"]) { + define([], function () { return jdenticon; }); + } + // No module loader + else { + umdGlobal["jdenticon"] = jdenticon; + } +})(typeof self !== "undefined" ? self : this, function (umdGlobal) { +'use strict'; + +/** + * Parses a substring of the hash as a number. + * @param {number} startPosition + * @param {number=} octets + */ +function parseHex(hash, startPosition, octets) { + return parseInt(hash.substr(startPosition, octets), 16); +} + +function decToHex(v) { + v |= 0; // Ensure integer value + return v < 0 ? "00" : + v < 16 ? "0" + v.toString(16) : + v < 256 ? v.toString(16) : + "ff"; +} + +function hueToRgb(m1, m2, h) { + h = h < 0 ? h + 6 : h > 6 ? h - 6 : h; + return decToHex(255 * ( + h < 1 ? m1 + (m2 - m1) * h : + h < 3 ? m2 : + h < 4 ? m1 + (m2 - m1) * (4 - h) : + m1)); +} + +/** + * @param {string} color Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported. + * @returns {string} + */ +function parseColor(color) { + if (/^#[0-9a-f]{3,8}$/i.test(color)) { + var result; + var colorLength = color.length; + + if (colorLength < 6) { + var r = color[1], + g = color[2], + b = color[3], + a = color[4] || ""; + result = "#" + r + r + g + g + b + b + a + a; + } + if (colorLength == 7 || colorLength > 8) { + result = color; + } + + return result; + } +} + +/** + * Converts a hexadecimal color to a CSS3 compatible color. + * @param {string} hexColor Color on the format "#RRGGBB" or "#RRGGBBAA" + * @returns {string} + */ +function toCss3Color(hexColor) { + var a = parseHex(hexColor, 7, 2); + var result; + + if (isNaN(a)) { + result = hexColor; + } else { + var r = parseHex(hexColor, 1, 2), + g = parseHex(hexColor, 3, 2), + b = parseHex(hexColor, 5, 2); + result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")"; + } + + return result; +} + +/** + * Converts an HSL color to a hexadecimal RGB color. + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ +function hsl(hue, saturation, lightness) { + // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color + var result; + + if (saturation == 0) { + var partialHex = decToHex(lightness * 255); + result = partialHex + partialHex + partialHex; + } + else { + var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation, + m1 = lightness * 2 - m2; + result = + hueToRgb(m1, m2, hue * 6 + 2) + + hueToRgb(m1, m2, hue * 6) + + hueToRgb(m1, m2, hue * 6 - 2); + } + + return "#" + result; +} + +/** + * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ +function correctedHsl(hue, saturation, lightness) { + // The corrector specifies the perceived middle lightness for each hue + var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], + corrector = correctors[(hue * 6 + 0.5) | 0]; + + // Adjust the input lightness relative to the corrector + lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2; + + return hsl(hue, saturation, lightness); +} + +/* global umdGlobal */ + +// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for +// backward compatibility. +var GLOBAL = umdGlobal; + +/** + * @typedef {Object} ParsedConfiguration + * @property {number} colorSaturation + * @property {number} grayscaleSaturation + * @property {string} backColor + * @property {number} iconPadding + * @property {function(number):number} hue + * @property {function(number):number} colorLightness + * @property {function(number):number} grayscaleLightness + */ + +var CONFIG_PROPERTIES = { + G/*GLOBAL*/: "jdenticon_config", + n/*MODULE*/: "config", +}; + +var rootConfigurationHolder = {}; + +/** + * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console + * when it is being used. + * @param {!Object} rootObject + */ +function defineConfigProperty(rootObject) { + rootConfigurationHolder = rootObject; +} + +/** + * Sets a new icon style configuration. The new configuration is not merged with the previous one. * + * @param {Object} newConfiguration - New configuration object. + */ +function configure(newConfiguration) { + if (arguments.length) { + rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration; + } + return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/]; +} + +/** + * Gets the normalized current Jdenticon color configuration. Missing fields have default values. + * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A + * local configuration overrides the global configuration in it entirety. This parameter can for backward + * compatibility also contain a padding value. A padding value only overrides the global padding, not the + * entire global configuration. + * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor + * explicitly to the API method. + * @returns {ParsedConfiguration} + */ +function getConfiguration(paddingOrLocalConfig, defaultPadding) { + var configObject = + typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig || + rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] || + GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || + { }, + + lightnessConfig = configObject["lightness"] || { }, + + // In versions < 2.1.0 there was no grayscale saturation - + // saturation was the color saturation. + saturation = configObject["saturation"] || { }, + colorSaturation = "color" in saturation ? saturation["color"] : saturation, + grayscaleSaturation = saturation["grayscale"], + + backColor = configObject["backColor"], + padding = configObject["padding"]; + + /** + * Creates a lightness range. + */ + function lightness(configName, defaultRange) { + var range = lightnessConfig[configName]; + + // Check if the lightness range is an array-like object. This way we ensure the + // array contain two values at the same time. + if (!(range && range.length > 1)) { + range = defaultRange; + } + + /** + * Gets a lightness relative the specified value in the specified lightness range. + */ + return function (value) { + value = range[0] + value * (range[1] - range[0]); + return value < 0 ? 0 : value > 1 ? 1 : value; + }; + } + + /** + * Gets a hue allowed by the configured hue restriction, + * provided the originally computed hue. + */ + function hueFunction(originalHue) { + var hueConfig = configObject["hues"]; + var hue; + + // Check if 'hues' is an array-like object. This way we also ensure that + // the array is not empty, which would mean no hue restriction. + if (hueConfig && hueConfig.length > 0) { + // originalHue is in the range [0, 1] + // Multiply with 0.999 to change the range to [0, 1) and then truncate the index. + hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)]; + } + + return typeof hue == "number" ? + + // A hue was specified. We need to convert the hue from + // degrees on any turn - e.g. 746° is a perfectly valid hue - + // to turns in the range [0, 1). + ((((hue / 360) % 1) + 1) % 1) : + + // No hue configured => use original hue + originalHue; + } + + return { + X/*hue*/: hueFunction, + p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5, + H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0, + q/*colorLightness*/: lightness("color", [0.4, 0.8]), + I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]), + J/*backColor*/: parseColor(backColor), + Y/*iconPadding*/: + typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig : + typeof padding == "number" ? padding : + defaultPadding + } +} + +var ICON_TYPE_SVG = 1; + +var ICON_TYPE_CANVAS = 2; + +var ATTRIBUTES = { + t/*HASH*/: "data-jdenticon-hash", + o/*VALUE*/: "data-jdenticon-value" +}; + +var ICON_SELECTOR = "[" + ATTRIBUTES.t/*HASH*/ +"],[" + ATTRIBUTES.o/*VALUE*/ +"]"; + +var documentQuerySelectorAll = /** @type {!Function} */ ( + typeof document !== "undefined" && document.querySelectorAll.bind(document)); + +function getIdenticonType(el) { + if (el) { + var tagName = el["tagName"]; + + if (/^svg$/i.test(tagName)) { + return ICON_TYPE_SVG; + } + + if (/^canvas$/i.test(tagName) && "getContext" in el) { + return ICON_TYPE_CANVAS; + } + } +} + +function observer(updateCallback) { + if (typeof MutationObserver != "undefined") { + var mutationObserver = new MutationObserver(function onmutation(mutations) { + for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) { + var mutation = mutations[mutationIndex]; + var addedNodes = mutation.addedNodes; + + for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) { + var addedNode = addedNodes[addedNodeIndex]; + + // Skip other types of nodes than element nodes, since they might not support + // the querySelectorAll method => runtime error. + if (addedNode.nodeType == 1) { + if (getIdenticonType(addedNode)) { + updateCallback(addedNode); + } + else { + var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR); + for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) { + updateCallback(icons[iconIndex]); + } + } + } + } + + if (mutation.type == "attributes" && getIdenticonType(mutation.target)) { + updateCallback(mutation.target); + } + } + }); + + mutationObserver.observe(document.body, { + "childList": true, + "attributes": true, + "attributeFilter": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, "width", "height"], + "subtree": true, + }); + } +} + +/** + * Represents a point. + */ +function Point(x, y) { + this.x = x; + this.y = y; +} + +/** + * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, + * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. + */ +function Transform(x, y, size, rotation) { + this.u/*_x*/ = x; + this.v/*_y*/ = y; + this.K/*_size*/ = size; + this.Z/*_rotation*/ = rotation; +} + +/** + * Transforms the specified point based on the translation and rotation specification for this Transform. + * @param {number} x x-coordinate + * @param {number} y y-coordinate + * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + */ +Transform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) { + var right = this.u/*_x*/ + this.K/*_size*/, + bottom = this.v/*_y*/ + this.K/*_size*/, + rotation = this.Z/*_rotation*/; + return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) : + rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : + rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) : + new Point(this.u/*_x*/ + x, this.v/*_y*/ + y); +}; + +var NO_TRANSFORM = new Transform(0, 0, 0, 0); + + + +/** + * Provides helper functions for rendering common basic shapes. + */ +function Graphics(renderer) { + /** + * @type {Renderer} + * @private + */ + this.M/*_renderer*/ = renderer; + + /** + * @type {Transform} + */ + this.A/*currentTransform*/ = NO_TRANSFORM; +} +var Graphics__prototype = Graphics.prototype; + +/** + * Adds a polygon to the underlying renderer. + * @param {Array} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] + * @param {boolean=} invert Specifies if the polygon will be inverted. + */ +Graphics__prototype.g/*addPolygon*/ = function addPolygon (points, invert) { + var this$1 = this; + + var di = invert ? -2 : 2, + transformedPoints = []; + + for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { + transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1])); + } + + this.M/*_renderer*/.g/*addPolygon*/(transformedPoints); +}; + +/** + * Adds a polygon to the underlying renderer. + * Source: http://stackoverflow.com/a/2173084 + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} size The size of the ellipse. + * @param {boolean=} invert Specifies if the ellipse will be inverted. + */ +Graphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) { + var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size); + this.M/*_renderer*/.h/*addCircle*/(p, size, invert); +}; + +/** + * Adds a rectangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle. + * @param {number} w The width of the rectangle. + * @param {number} h The height of the rectangle. + * @param {boolean=} invert Specifies if the rectangle will be inverted. + */ +Graphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) { + this.g/*addPolygon*/([ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ], invert); +}; + +/** + * Adds a right triangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} w The width of the triangle. + * @param {number} h The height of the triangle. + * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. + * @param {boolean=} invert Specifies if the triangle will be inverted. + */ +Graphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) { + var points = [ + x + w, y, + x + w, y + h, + x, y + h, + x, y + ]; + points.splice(((r || 0) % 4) * 2, 2); + this.g/*addPolygon*/(points, invert); +}; + +/** + * Adds a rhombus to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} w The width of the rhombus. + * @param {number} h The height of the rhombus. + * @param {boolean=} invert Specifies if the rhombus will be inverted. + */ +Graphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) { + this.g/*addPolygon*/([ + x + w / 2, y, + x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2 + ], invert); +}; + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + * @param {number} positionIndex + */ +function centerShape(index, g, cell, positionIndex) { + index = index % 14; + + var k, m, w, h, inner, outer; + + !index ? ( + k = cell * 0.42, + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell - k * 2, + cell - k, cell, + 0, cell + ])) : + + index == 1 ? ( + w = 0 | (cell * 0.5), + h = 0 | (cell * 0.8), + + g.j/*addTriangle*/(cell - w, 0, w, h, 2)) : + + index == 2 ? ( + w = 0 | (cell / 3), + g.i/*addRectangle*/(w, w, cell - w, cell - w)) : + + index == 3 ? ( + inner = cell * 0.1, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 6 ? 1 : + cell < 8 ? 2 : + (0 | (cell * 0.25)), + + inner = + inner > 1 ? (0 | inner) : // large icon => truncate decimals + inner > 0.5 ? 1 : // medium size icon => fixed width + inner, // small icon => anti-aliased border + + g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) : + + index == 4 ? ( + m = 0 | (cell * 0.15), + w = 0 | (cell * 0.5), + g.h/*addCircle*/(cell - w - m, cell - w - m, w)) : + + index == 5 ? ( + inner = cell * 0.1, + outer = inner * 4, + + // Align edge to nearest pixel in large icons + outer > 3 && (outer = 0 | outer), + + g.i/*addRectangle*/(0, 0, cell, cell), + g.g/*addPolygon*/([ + outer, outer, + cell - inner, outer, + outer + (cell - outer - inner) / 2, cell - inner + ], true)) : + + index == 6 ? + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell * 0.7, + cell * 0.4, cell * 0.4, + cell * 0.7, cell, + 0, cell + ]) : + + index == 7 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 8 ? ( + g.i/*addRectangle*/(0, 0, cell, cell / 2), + g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2), + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) : + + index == 9 ? ( + inner = cell * 0.14, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 4 ? 1 : + cell < 6 ? 2 : + (0 | (cell * 0.35)), + + inner = + cell < 8 ? inner : // small icon => anti-aliased border + (0 | inner), // large icon => truncate decimals + + g.i/*addRectangle*/(0, 0, cell, cell), + g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) : + + index == 10 ? ( + inner = cell * 0.12, + outer = inner * 3, + + g.i/*addRectangle*/(0, 0, cell, cell), + g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) : + + index == 11 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 12 ? ( + m = cell * 0.25, + g.i/*addRectangle*/(0, 0, cell, cell), + g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) : + + // 13 + ( + !positionIndex && ( + m = cell * 0.4, w = cell * 1.2, + g.h/*addCircle*/(m, m, w) + ) + ); +} + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + */ +function outerShape(index, g, cell) { + index = index % 4; + + var m; + + !index ? + g.j/*addTriangle*/(0, 0, cell, cell, 0) : + + index == 1 ? + g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) : + + index == 2 ? + g.N/*addRhombus*/(0, 0, cell, cell) : + + // 3 + ( + m = cell / 6, + g.h/*addCircle*/(m, m, cell - 2 * m) + ); +} + +/** + * Gets a set of identicon color candidates for a specified hue and config. + * @param {number} hue + * @param {ParsedConfiguration} config + */ +function colorTheme(hue, config) { + hue = config.X/*hue*/(hue); + return [ + // Dark gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)), + // Mid color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)), + // Light gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)), + // Light color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)), + // Dark color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0)) + ]; +} + +/** + * Draws an identicon to a specified renderer. + * @param {Renderer} renderer + * @param {string} hash + * @param {Object|number=} config + */ +function iconGenerator(renderer, hash, config) { + var parsedConfig = getConfiguration(config, 0.08); + + // Set background color + if (parsedConfig.J/*backColor*/) { + renderer.m/*setBackground*/(parsedConfig.J/*backColor*/); + } + + // Calculate padding and round to nearest integer + var size = renderer.k/*iconSize*/; + var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0; + size -= padding * 2; + + var graphics = new Graphics(renderer); + + // Calculate cell size and ensure it is an integer + var cell = 0 | (size / 4); + + // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon + var x = 0 | (padding + size / 2 - cell * 2); + var y = 0 | (padding + size / 2 - cell * 2); + + function renderShape(colorIndex, shapes, index, rotationIndex, positions) { + var shapeIndex = parseHex(hash, index, 1); + var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0; + + renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]); + + for (var i = 0; i < positions.length; i++) { + graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); + shapes(shapeIndex, graphics, cell, i); + } + + renderer.P/*endShape*/(); + } + + // AVAILABLE COLORS + var hue = parseHex(hash, -7) / 0xfffffff, + + // Available colors for this icon + availableColors = colorTheme(hue, parsedConfig), + + // The index of the selected colors + selectedColorIndexes = []; + + var index; + + function isDuplicate(values) { + if (values.indexOf(index) >= 0) { + for (var i = 0; i < values.length; i++) { + if (selectedColorIndexes.indexOf(values[i]) >= 0) { + return true; + } + } + } + } + + for (var i = 0; i < 3; i++) { + index = parseHex(hash, 8 + i, 1) % availableColors.length; + if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo + isDuplicate([2, 3])) { // Disallow light gray and light color combo + index = 1; + } + selectedColorIndexes.push(index); + } + + // ACTUAL RENDERING + // Sides + renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); + // Corners + renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); + // Center + renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); + + renderer.finish(); +} + +/** + * Computes a SHA1 hash for any value and returns it as a hexadecimal string. + * + * This function is optimized for minimal code size and rather short messages. + * + * @param {string} message + */ +function sha1(message) { + var HASH_SIZE_HALF_BYTES = 40; + var BLOCK_SIZE_WORDS = 16; + + // Variables + // `var` is used to be able to minimize the number of `var` keywords. + var i = 0, + f = 0, + + // Use `encodeURI` to UTF8 encode the message without any additional libraries + // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky + // since `unescape` is deprecated. + urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding + + // This can be changed to a preallocated Uint32Array array for greater performance and larger code size + data = [], + dataSize, + + hashBuffer = [], + + a = 0x67452301, + b = 0xefcdab89, + c = ~a, + d = ~b, + e = 0xc3d2e1f0, + hash = [a, b, c, d, e], + + blockStartIndex = 0, + hexHash = ""; + + /** + * Rotates the value a specified number of bits to the left. + * @param {number} value Value to rotate + * @param {number} shift Bit count to shift. + */ + function rotl(value, shift) { + return (value << shift) | (value >>> (32 - shift)); + } + + // Message data + for ( ; i < urlEncodedMessage.length; f++) { + data[f >> 2] = data[f >> 2] | + ( + ( + urlEncodedMessage[i] == "%" + // Percent encoded byte + ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16) + // Unencoded byte + : urlEncodedMessage.charCodeAt(i++) + ) + + // Read bytes in reverse order (big endian words) + << ((3 - (f & 3)) * 8) + ); + } + + // f is now the length of the utf8 encoded message + // 7 = 8 bytes (64 bit) for message size, -1 to round down + // >> 6 = integer division with block size + dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS; + + // Message size in bits. + // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least + // significant 32 bits are set. -8 is for the '1' bit padding byte. + data[dataSize - 1] = f * 8 - 8; + + // Compute hash + for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) { + for (i = 0; i < 80; i++) { + f = rotl(a, 5) + e + ( + // Ch + i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 : + + // Parity + i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 : + + // Maj + i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc : + + // Parity + (b ^ c ^ d) + 0xca62c1d6 + ) + ( + hashBuffer[i] = i < BLOCK_SIZE_WORDS + // Bitwise OR is used to coerse `undefined` to 0 + ? (data[blockStartIndex + i] | 0) + : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1) + ); + + e = d; + d = c; + c = rotl(b, 30); + b = a; + a = f; + } + + hash[0] = a = ((hash[0] + a) | 0); + hash[1] = b = ((hash[1] + b) | 0); + hash[2] = c = ((hash[2] + c) | 0); + hash[3] = d = ((hash[3] + d) | 0); + hash[4] = e = ((hash[4] + e) | 0); + } + + // Format hex hash + for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) { + hexHash += ( + ( + // Get word (2^3 half-bytes per word) + hash[i >> 3] >>> + + // Append half-bytes in reverse order + ((7 - (i & 7)) * 4) + ) + // Clamp to half-byte + & 0xf + ).toString(16); + } + + return hexHash; +} + +/** + * Inputs a value that might be a valid hash string for Jdenticon and returns it + * if it is determined valid, otherwise a falsy value is returned. + */ +function isValidHash(hashCandidate) { + return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate; +} + +/** + * Computes a hash for the specified value. Currently SHA1 is used. This function + * always returns a valid hash. + */ +function computeHash(value) { + return sha1(value == null ? "" : "" + value); +} + + + +/** + * Renderer redirecting drawing commands to a canvas context. + * @implements {Renderer} + */ +function CanvasRenderer(ctx, iconSize) { + var canvas = ctx.canvas; + var width = canvas.width; + var height = canvas.height; + + ctx.save(); + + if (!iconSize) { + iconSize = Math.min(width, height); + + ctx.translate( + ((width - iconSize) / 2) | 0, + ((height - iconSize) / 2) | 0); + } + + /** + * @private + */ + this.l/*_ctx*/ = ctx; + this.k/*iconSize*/ = iconSize; + + ctx.clearRect(0, 0, iconSize, iconSize); +} +var CanvasRenderer__prototype = CanvasRenderer.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ +CanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { + var ctx = this.l/*_ctx*/; + var iconSize = this.k/*iconSize*/; + + ctx.fillStyle = toCss3Color(fillColor); + ctx.fillRect(0, 0, iconSize, iconSize); +}; + +/** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} fillColor Fill color on format #rrggbb[aa]. + */ +CanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) { + var ctx = this.l/*_ctx*/; + ctx.fillStyle = toCss3Color(fillColor); + ctx.beginPath(); +}; + +/** + * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. + */ +CanvasRenderer__prototype.P/*endShape*/ = function endShape () { + this.l/*_ctx*/.fill(); +}; + +/** + * Adds a polygon to the rendering queue. + * @param points An array of Point objects. + */ +CanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { + var ctx = this.l/*_ctx*/; + ctx.moveTo(points[0].x, points[0].y); + for (var i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y); + } + ctx.closePath(); +}; + +/** + * Adds a circle to the rendering queue. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +CanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + var ctx = this.l/*_ctx*/, + radius = diameter / 2; + ctx.moveTo(point.x + radius, point.y + radius); + ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); + ctx.closePath(); +}; + +/** + * Called when the icon has been completely drawn. + */ +CanvasRenderer__prototype.finish = function finish () { + this.l/*_ctx*/.restore(); +}; + +/** + * Draws an identicon to a context. + * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0). + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @param {number} size - Icon size in pixels. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function drawIcon(ctx, hashOrValue, size, config) { + if (!ctx) { + throw new Error("No canvas specified."); + } + + iconGenerator(new CanvasRenderer(ctx, size), + isValidHash(hashOrValue) || computeHash(hashOrValue), + config); +} + +/** + * Prepares a measure to be used as a measure in an SVG path, by + * rounding the measure to a single decimal. This reduces the file + * size of the generated SVG with more than 50% in some cases. + */ +function svgValue(value) { + return ((value * 10 + 0.5) | 0) / 10; +} + +/** + * Represents an SVG path element. + */ +function SvgPath() { + /** + * This property holds the data string (path.d) of the SVG path. + * @type {string} + */ + this.B/*dataString*/ = ""; +} +var SvgPath__prototype = SvgPath.prototype; + +/** + * Adds a polygon with the current fill color to the SVG path. + * @param points An array of Point objects. + */ +SvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) { + var dataString = ""; + for (var i = 0; i < points.length; i++) { + dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y); + } + this.B/*dataString*/ += dataString + "Z"; +}; + +/** + * Adds a circle with the current fill color to the SVG path. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +SvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + var sweepFlag = counterClockwise ? 0 : 1, + svgRadius = svgValue(diameter / 2), + svgDiameter = svgValue(diameter), + svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " "; + + this.B/*dataString*/ += + "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) + + svgArc + svgDiameter + ",0" + + svgArc + (-svgDiameter) + ",0"; +}; + + + +/** + * Renderer producing SVG output. + * @implements {Renderer} + */ +function SvgRenderer(target) { + /** + * @type {SvgPath} + * @private + */ + this.C/*_path*/; + + /** + * @type {Object.} + * @private + */ + this.D/*_pathsByColor*/ = { }; + + /** + * @type {SvgElement|SvgWriter} + * @private + */ + this.R/*_target*/ = target; + + /** + * @type {number} + */ + this.k/*iconSize*/ = target.k/*iconSize*/; +} +var SvgRenderer__prototype = SvgRenderer.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ +SvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { + var match = /^(#......)(..)?/.exec(fillColor), + opacity = match[2] ? parseHex(match[2], 0) / 255 : 1; + this.R/*_target*/.m/*setBackground*/(match[1], opacity); +}; + +/** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} color Fill color on format #xxxxxx. + */ +SvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) { + this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath()); +}; + +/** + * Marks the end of the currently drawn shape. + */ +SvgRenderer__prototype.P/*endShape*/ = function endShape () { }; + +/** + * Adds a polygon with the current fill color to the SVG. + * @param points An array of Point objects. + */ +SvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { + this.C/*_path*/.g/*addPolygon*/(points); +}; + +/** + * Adds a circle with the current fill color to the SVG. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +SvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise); +}; + +/** + * Called when the icon has been completely drawn. + */ +SvgRenderer__prototype.finish = function finish () { + var this$1 = this; + + var pathsByColor = this.D/*_pathsByColor*/; + for (var color in pathsByColor) { + // hasOwnProperty cannot be shadowed in pathsByColor + // eslint-disable-next-line no-prototype-builtins + if (pathsByColor.hasOwnProperty(color)) { + this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/); + } + } +}; + +var SVG_CONSTANTS = { + T/*XMLNS*/: "http://www.w3.org/2000/svg", + U/*WIDTH*/: "width", + V/*HEIGHT*/: "height", +}; + +/** + * Renderer producing SVG output. + */ +function SvgWriter(iconSize) { + /** + * @type {number} + */ + this.k/*iconSize*/ = iconSize; + + /** + * @type {string} + * @private + */ + this.F/*_s*/ = + ''; +} +var SvgWriter__prototype = SvgWriter.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ +SvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { + if (opacity) { + this.F/*_s*/ += ''; + } +}; + +/** + * Writes a path to the SVG string. + * @param {string} color Fill color on format #rrggbb. + * @param {string} dataString The SVG path data string. + */ +SvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) { + this.F/*_s*/ += ''; +}; + +/** + * Gets the rendered image as an SVG string. + */ +SvgWriter__prototype.toString = function toString () { + return this.F/*_s*/ + ""; +}; + +/** + * Draws an identicon as an SVG string. + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @param {number} size - Icon size in pixels. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + * @returns {string} SVG string + */ +function toSvg(hashOrValue, size, config) { + var writer = new SvgWriter(size); + iconGenerator(new SvgRenderer(writer), + isValidHash(hashOrValue) || computeHash(hashOrValue), + config); + return writer.toString(); +} + +/** + * Creates a new element and adds it to the specified parent. + * @param {Element} parentNode + * @param {string} name + * @param {...(string|number)} keyValuePairs + */ +function SvgElement_append(parentNode, name) { + var keyValuePairs = [], len = arguments.length - 2; + while ( len-- > 0 ) keyValuePairs[ len ] = arguments[ len + 2 ]; + + var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name); + + for (var i = 0; i + 1 < keyValuePairs.length; i += 2) { + el.setAttribute( + /** @type {string} */(keyValuePairs[i]), + /** @type {string} */(keyValuePairs[i + 1]) + ); + } + + parentNode.appendChild(el); +} + + +/** + * Renderer producing SVG output. + */ +function SvgElement(element) { + // Don't use the clientWidth and clientHeight properties on SVG elements + // since Firefox won't serve a proper value of these properties on SVG + // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811) + // Instead use 100px as a hardcoded size (the svg viewBox will rescale + // the icon to the correct dimensions) + var iconSize = this.k/*iconSize*/ = Math.min( + (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100), + (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100) + ); + + /** + * @type {Element} + * @private + */ + this.W/*_el*/ = element; + + // Clear current SVG child elements + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + // Set viewBox attribute to ensure the svg scales nicely. + element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize); + element.setAttribute("preserveAspectRatio", "xMidYMid meet"); +} +var SvgElement__prototype = SvgElement.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ +SvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { + if (opacity) { + SvgElement_append(this.W/*_el*/, "rect", + SVG_CONSTANTS.U/*WIDTH*/, "100%", + SVG_CONSTANTS.V/*HEIGHT*/, "100%", + "fill", fillColor, + "opacity", opacity); + } +}; + +/** + * Appends a path to the SVG element. + * @param {string} color Fill color on format #xxxxxx. + * @param {string} dataString The SVG path data string. + */ +SvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) { + SvgElement_append(this.W/*_el*/, "path", + "fill", color, + "d", dataString); +}; + +/** + * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute. + */ +function updateAll() { + if (documentQuerySelectorAll) { + update(ICON_SELECTOR); + } +} + +/** + * Updates the identicon in the specified `` or `` elements. + * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type + * `` or ``, or a CSS selector to such an element. + * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or + * `data-jdenticon-value` attribute will be evaluated. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function update(el, hashOrValue, config) { + renderDomElement(el, hashOrValue, config, function (el, iconType) { + if (iconType) { + return iconType == ICON_TYPE_SVG ? + new SvgRenderer(new SvgElement(el)) : + new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d")); + } + }); +} + +/** + * Updates the identicon in the specified canvas or svg elements. + * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type + * `` or ``, or a CSS selector to such an element. + * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or + * `data-jdenticon-value` attribute will be evaluated. + * @param {Object|number|undefined} config + * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer. + */ +function renderDomElement(el, hashOrValue, config, rendererFactory) { + if (typeof el === "string") { + if (documentQuerySelectorAll) { + var elements = documentQuerySelectorAll(el); + for (var i = 0; i < elements.length; i++) { + renderDomElement(elements[i], hashOrValue, config, rendererFactory); + } + } + return; + } + + // Hash selection. The result from getValidHash or computeHash is + // accepted as a valid hash. + var hash = + // 1. Explicit valid hash + isValidHash(hashOrValue) || + + // 2. Explicit value (`!= null` catches both null and undefined) + hashOrValue != null && computeHash(hashOrValue) || + + // 3. `data-jdenticon-hash` attribute + isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) || + + // 4. `data-jdenticon-value` attribute. + // We want to treat an empty attribute as an empty value. + // Some browsers return empty string even if the attribute + // is not specified, so use hasAttribute to determine if + // the attribute is specified. + el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/)); + + if (!hash) { + // No hash specified. Don't render an icon. + return; + } + + var renderer = rendererFactory(el, getIdenticonType(el)); + if (renderer) { + // Draw icon + iconGenerator(renderer, hash, config); + } +} + +/** + * Renders an identicon for all matching supported elements. + * + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not + * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be + * evaluated. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global + * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function jdenticonJqueryPlugin(hashOrValue, config) { + this["each"](function (index, el) { + update(el, hashOrValue, config); + }); + return this; +} + +// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js + +var jdenticon = updateAll; + +defineConfigProperty(jdenticon); + +// Export public API +jdenticon["configure"] = configure; +jdenticon["drawIcon"] = drawIcon; +jdenticon["toSvg"] = toSvg; +jdenticon["update"] = update; +jdenticon["updateCanvas"] = update; +jdenticon["updateSvg"] = update; + +/** + * Specifies the version of the Jdenticon package in use. + * @type {string} + */ +jdenticon["version"] = "3.2.0"; + +/** + * Specifies which bundle of Jdenticon that is used. + * @type {string} + */ +jdenticon["bundle"] = "browser-umd"; + +// Basic jQuery plugin +var jQuery = GLOBAL["jQuery"]; +if (jQuery) { + jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin; +} + +/** + * This function is called once upon page load. + */ +function jdenticonStartup() { + var replaceMode = ( + jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] || + GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || + { } + )["replaceMode"]; + + if (replaceMode != "never") { + updateAll(); + + if (replaceMode == "observe") { + observer(update); + } + } +} + +// Schedule to render all identicons on the page once it has been loaded. +if (typeof setTimeout === "function") { + setTimeout(jdenticonStartup, 0); +} + +return jdenticon; + +}); \ No newline at end of file diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs index 1549904d..220b562d 100644 --- a/src/static/templates/admin/base.hbs +++ b/src/static/templates/admin/base.hbs @@ -28,7 +28,7 @@ border: var(--bs-alert-border); } - + +
      {{Name}} ({{BillingEmail}}) @@ -73,12 +73,6 @@ return false; } - (async () => { - for (let e of document.querySelectorAll("img.identicon")) { - e.src = await identicon(e.dataset.src); - } - })(); - document.addEventListener("DOMContentLoaded", function() { $('#orgs-table').DataTable({ "responsive": true, diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 93f0dd4d..64e4188d 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -19,7 +19,7 @@ {{#each page_data}} - +
      {{Name}} {{Email}} @@ -206,12 +206,6 @@ "3": { "name": "Manager", "color": "green" }, }; - (async () => { - for (let e of document.querySelectorAll("img.identicon")) { - e.src = await identicon(e.dataset.src); - } - })(); - document.querySelectorAll("[data-orgtype]").forEach(function (e) { let orgtype = OrgTypes[e.dataset.orgtype]; e.style.backgroundColor = orgtype.color; From d66323b742bd98e16e0fd0124de840ebb8564ea3 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 2 Dec 2022 16:25:11 +0100 Subject: [PATCH 4/5] Limit Cipher Note encrypted string size As discussed in #2937, this will limit the amount of encrypted characters to 10.000 characters, same as Bitwarden. This will not break current ciphers which exceed this limit, but it will prevent those ciphers from being updated. Fixes #2937 --- src/api/core/ciphers.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index c8c741d4..c72419b0 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -366,6 +366,12 @@ pub async fn update_cipher_from_data( err!("Organization mismatch. Please resync the client before updating the cipher") } + if let Some(note) = &data.Notes { + if note.len() > 10_000 { + err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.") + } + } + // Check if this cipher is being transferred from a personal to an organization vault let transfer_cipher = cipher.organization_uuid.is_none() && data.OrganizationId.is_some(); From d6dc6070f3cc4c1a19b65cdee8edd413c74df0ea Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 3 Dec 2022 17:28:25 +0100 Subject: [PATCH 5/5] Fix admin repost warning. Currently when you login into the admin, and then directly hit the save button, it will come with a re-post/re-submit warning. This has to do with the `window.location.reload()` function, which triggers the admin login POST again. By changing the way to reload the page, we prevent this repost. --- src/static/templates/admin/base.hbs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs index 220b562d..8b5e891b 100644 --- a/src/static/templates/admin/base.hbs +++ b/src/static/templates/admin/base.hbs @@ -32,7 +32,11 @@