From 32d85d03bb5ec401d1378f4cd60139be1d8db3f4 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:13:32 +0200 Subject: [PATCH 01/20] Fix organization import failing with missing field groups (#7699) --- src/api/core/organizations.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 9082297f..c0c90426 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -132,7 +132,6 @@ struct FullCollectionData { name: String, groups: Vec, users: Vec, - id: Option, external_id: Option, } @@ -1793,11 +1792,22 @@ async fn bulk_public_keys( use super::ciphers::CipherData; use super::ciphers::update_cipher_from_data; +// The import endpoint only ever uses the name/id/external_id of a collection. +// Bitwarden's own server ignores `groups`/`users` here too, so do not make them +// mandatory: clients are free to leave them out. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ImportCollectionData { + name: String, + id: Option, + external_id: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ImportData { ciphers: Vec, - collections: Vec, + collections: Vec, collection_relationships: Vec, } From 2ffad8775d8712329aab7d00a05a94f64098170b Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:13:40 +0200 Subject: [PATCH 02/20] Add `pm-32413-multi-client-password-management` feature flag (#7677) --- .env.template | 1 + src/config.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index 5f6f374c..d22145b8 100644 --- a/.env.template +++ b/.env.template @@ -390,6 +390,7 @@ ## ## The following flags are available: ## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Safari >= 2026.2.0) +## - "pm-32413-multi-client-password-management": Enable changing the master password directly in the client. (Desktop/Extension >= 2026.4.0) ## - "ssh-agent": Enable SSH agent support on Desktop. (Desktop >= 2024.12.0) ## - "ssh-agent-v2": Enable newer SSH agent support. (Desktop >= 2026.2.1) ## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Clients >= 2024.12.0) diff --git a/src/config.rs b/src/config.rs index 87bea195..7e21ecf1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1425,6 +1425,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "desktop-ui-migration-milestone-4", // Auth Team "pm-5594-safari-account-switching", + "pm-32413-multi-client-password-management", // Autofill Team "ssh-agent", "ssh-agent-v2", From 277e1536ebe426296519f8bd8bf99f5f6c1c5c77 Mon Sep 17 00:00:00 2001 From: The CRahn <5043504+crahn@users.noreply.github.com> Date: Tue, 8 Sep 2026 05:13:51 -0500 Subject: [PATCH 03/20] Log IP/username on two-factor email-login credential failures (#7654) The three "Username or password is incorrect" errors in send_email_login() (email.rs) don't log the client IP or submitted identifier, unlike the equivalent wrong-password error in password_login() (identity.rs), which logs both via format!("IP: {}. Username: {username}.", ip.ip). This makes the two code paths inconsistent for the same underlying error, and means log-based tooling that keys on the identity.rs error's "IP: x.x.x.x" pattern can't do the same for this endpoint. Bring email.rs's three call sites in line with identity.rs's existing format. The two email-present branches log IP+Username (the email submitted); the device-identifier-only branch (SSO path, no email in scope) logs IP+Device instead of fabricating a username. Verified: cargo build/test/clippy/fmt all pass with the sqlite feature (matching one leg of this repo's own CI matrix), including the two existing unit tests in this file. --- src/api/core/two_factor/email.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 44ba2e7f..3667b871 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -63,13 +63,19 @@ async fn send_email_login(data: Json, client_headers: Client let user = if let Some(email) = email { let Some(user) = User::find_by_mail(email, &conn).await else { - err!("Username or password is incorrect. Try again.") + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Username: {email}.", client_headers.ip.ip) + ) }; if let Some(master_password_hash) = master_password_hash { // Check password if !user.check_valid_password(master_password_hash) { - err!("Username or password is incorrect. Try again.") + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Username: {email}.", client_headers.ip.ip) + ) } } else if let Some(auth_request_id) = auth_request_id { let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_id, &conn).await else { @@ -96,7 +102,10 @@ async fn send_email_login(data: Json, client_headers: Client }; // SSO login only sends device id, so we get the user by the most recently used device let Some(user) = User::find_by_device_for_email2fa(device_identifier, &conn).await else { - err!("Username or password is incorrect. Try again.") + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Device: {device_identifier}.", client_headers.ip.ip) + ) }; user From 57fbed1bed2e42b540cb790dd536e02633c4f445 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 8 Sep 2026 10:14:01 +0000 Subject: [PATCH 04/20] Support admin reset 2fa (#7435) * Support admin reset 2fa * Fix recovery email --------- Co-authored-by: Timshel --- playwright/tests/organization.smtp.spec.ts | 7 +- src/api/core/organizations.rs | 89 ++++++++++++------- src/auth.rs | 12 ++- src/config.rs | 2 +- src/db/models/event.rs | 8 +- src/mail.rs | 14 ++- .../email/admin_account_recovery.hbs | 12 +++ ...ml.hbs => admin_account_recovery.html.hbs} | 11 ++- .../templates/email/admin_reset_password.hbs | 4 - 9 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 src/static/templates/email/admin_account_recovery.hbs rename src/static/templates/email/{admin_reset_password.html.hbs => admin_account_recovery.html.hbs} (55%) delete mode 100644 src/static/templates/email/admin_reset_password.hbs diff --git a/playwright/tests/organization.smtp.spec.ts b/playwright/tests/organization.smtp.spec.ts index 6d0eb859..1e97ed5d 100644 --- a/playwright/tests/organization.smtp.spec.ts +++ b/playwright/tests/organization.smtp.spec.ts @@ -127,6 +127,9 @@ test('Organization is visible', async ({ page }) => { }); test('Recover user password', async ({ page }) => { + await logUser(test, page, users.user2, { mailBuffer: mail2Buffer }); + await activateTOTP(test, page, users.user2); + await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); let newPassword = "TotoNewPassword"; @@ -138,9 +141,10 @@ test('Recover user password', async ({ page }) => { await page.getByRole('menuitem', { name: 'Recover account' }).click(); await page.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword); await page.getByRole('textbox', { name: 'Confirm new master password * (' }).fill(newPassword); + await page.getByRole('checkbox', { name: 'Reset two-step login' }).check(); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Account recovery success'); - await mail2Buffer.expect((m) => m.subject.includes('Master Password Has Been Changed')); + await mail2Buffer.expect((m) => m.subject.includes('Admin account recovery from Test organization')); }); let user2 = { @@ -150,6 +154,7 @@ test('Recover user password', async ({ page }) => { }; await logUser(test, page, user2, { mailBuffer: mail2Buffer, + mail2fa: true, notNewDevice: true, }); }); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index c0c90426..4f490854 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use num_traits::FromPrimitive; -use rocket::{Route, serde::json::Json}; +use rocket::{Route, http::Status, serde::json::Json}; use serde_json::Value; use crate::{ @@ -17,7 +17,8 @@ use crate::{ models::{ Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, - OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId, + OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, TwoFactor, TwoFactorType, User, + UserId, }, }, mail, @@ -390,7 +391,7 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos } if !headers.membership.has_full_access() { - err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have full access", Status::NotFound.code); } Ok(Json(json!({ @@ -886,11 +887,11 @@ struct OrgIdData { #[get("/ciphers/organization-details?")] async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { if data.organization_id != headers.membership.org_uuid { - err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "Organization id's do not match", Status::NotFound.code); } if !headers.membership.has_full_access() { - err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have full access", Status::NotFound.code); } Ok(Json(json!({ @@ -954,7 +955,7 @@ async fn get_members( } if !headers.membership.has_full_access() { - err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have full access", Status::NotFound.code); } let mut users_json = Vec::new(); @@ -2486,7 +2487,7 @@ async fn get_groups_data( || Collection::has_manageable_collection_by_user(&org_id, &headers.membership.user_uuid, &conn).await }; if !allowed { - err_code!("Resource not found.", "User does not have access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have access", Status::NotFound.code); } let groups: Vec = if CONFIG.org_groups_enabled() { @@ -2937,8 +2938,8 @@ struct OrganizationUserResetPasswordEnrollmentRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OrganizationUserRecoverAccountRequest { - new_master_password_hash: String, - key: String, + new_master_password_hash: Option, + key: Option, #[serde(default)] reset_master_password: bool, @@ -2982,12 +2983,7 @@ async fn put_recover_account( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let req = data.into_inner(); - if req.reset_master_password && !req.reset_two_factor { - recover_account(org_id, member_id, headers, req, conn, nt).await - } else { - err!("Unsupported operation") - } + recover_account(org_id, member_id, headers, data.into_inner(), conn, nt).await } // Deprecated since `v2026.4.2` @@ -3007,7 +3003,7 @@ async fn recover_account( org_id: OrganizationId, member_id: MembershipId, headers: AdminHeaders, - reset_request: OrganizationUserRecoverAccountRequest, + req: OrganizationUserRecoverAccountRequest, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { @@ -3022,7 +3018,7 @@ async fn recover_account( err!("User to reset isn't member of required organization") }; - let Some(user) = User::find_by_uuid(&member.user_uuid, &conn).await else { + let Some(mut user) = User::find_by_uuid(&member.user_uuid, &conn).await else { err!("User not found") }; @@ -3035,29 +3031,56 @@ async fn recover_account( err!("Organization user must be confirmed for password reset functionality"); } - // Sending email before resetting password to ensure working email configuration and the resulting - // user notification. Also this might add some protection against security flaws and misuse - if let Err(e) = mail::send_admin_reset_password(&user.email, user.display_name(), &org.name).await { + let fallback_2fa_email = if req.reset_two_factor && CONFIG.email_2fa_auto_fallback() { + TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &conn).await.is_none() + } else { + false + }; + + // Sending email first ensure working email configuration and the resulting user notification. + // Also this might add some protection against security flaws and misuse + if let Err(e) = mail::send_admin_account_recovery( + &user.email, + user.display_name(), + &org.name, + req.reset_master_password, + req.reset_two_factor, + fallback_2fa_email, + ) + .await + { err!(format!("Error sending user reset password email: {e:#?}")); } - let mut user = user; - user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None, &conn) - .await?; + if req.reset_master_password { + if let Some(key) = req.key + && let Some(hash) = req.new_master_password_hash + { + user.set_password(hash.as_str(), Some(key), true, None, &conn).await?; + } else { + err_code!("Unprocessable request", "Missing fields to reset password", Status::UnprocessableEntity.code); + } + } + + if req.reset_two_factor { + TwoFactor::delete_all_by_user(&user.uuid, &conn).await?; + if !fallback_2fa_email || two_factor::email::find_and_activate_email_2fa(&user.uuid, &conn).await.is_err() { + two_factor::enforce_2fa_policy(&user, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn) + .await?; + } + } + user.save(&conn).await?; nt.send_logout(&user, None, &conn).await; - log_event( - EventType::OrganizationUserAdminResetPassword, - &member_id, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; + if req.reset_master_password { + headers.log_event(EventType::OrganizationUserAdminResetPassword, &member_id, &org_id, &conn).await; + } + + if req.reset_two_factor { + headers.log_event(EventType::OrganizationUserAdminResetTwoFactor, &member_id, &org_id, &conn).await; + } Ok(()) } diff --git a/src/auth.rs b/src/auth.rs index 762088e5..07373389 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -23,14 +23,14 @@ use rocket::{ use crate::{ CONFIG, - api::ApiResult, + api::{ApiResult, core::log_event}, config::PathType, db::{ DbConn, models::{ AttachmentId, CipherId, Collection, CollectionId, Device, DeviceId, DeviceType, EmergencyAccessId, - Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, OrganizationId, SendFileId, - SendId, User, UserId, UserStampException, + EventType, Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, OrganizationId, + SendFileId, SendId, User, UserId, UserStampException, }, }, error::Error, @@ -822,6 +822,12 @@ pub struct AdminHeaders { pub org_id: OrganizationId, } +impl AdminHeaders { + pub async fn log_event(&self, event_type: EventType, source_uuid: &str, org_id: &OrganizationId, conn: &DbConn) { + log_event(event_type, source_uuid, org_id, &self.user.uuid, self.device.atype, &self.ip.ip, conn).await; + } +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for AdminHeaders { type Error = &'static str; diff --git a/src/config.rs b/src/config.rs index 7e21ecf1..37fc3e85 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1745,7 +1745,7 @@ where reg!("email/email_footer"); reg!("email/email_footer_text"); - reg!("email/admin_reset_password", ".html"); + reg!("email/admin_account_recovery", ".html"); reg!("email/change_email_existing", ".html"); reg!("email/change_email_invited", ".html"); reg!("email/change_email", ".html"); diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 86cbf5d0..1f307979 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -43,7 +43,7 @@ pub struct Event { pub provider_org_uuid: Option, } -// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs +// Upstream enum: https://github.com/bitwarden/server/blob/v2026.6.2/src/Core/Dirt/Enums/EventType.cs #[derive(Debug, Copy, Clone)] pub enum EventType { // User @@ -108,6 +108,12 @@ pub enum EventType { OrganizationUserRejectedAuthRequest = 1514, OrganizationUserDeleted = 1515, // Both user and organization user data were deleted OrganizationUserLeft = 1516, // User voluntarily left the organization + // OrganizationUserAutomaticallyConfirmed = 1517, + // OrganizationUserSelfRevoked = 1518, // User self-revoked due to declining organization data ownership policy + OrganizationUserAdminResetTwoFactor = 1519, + // OrganizationUserRevoked_TwoFactorNonCompliance = 1520, + // OrganizationUserRevoked_SingleOrganizationNonCompliance = 1521, + // OrganizationUserNotificationBannerActionClicked = 1522, // Organization OrganizationUpdated = 1600, diff --git a/src/mail.rs b/src/mail.rs index a7e5e5ae..b20f2853 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -633,14 +633,24 @@ pub async fn send_test(address: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } -pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: &str) -> EmptyResult { +pub async fn send_admin_account_recovery( + address: &str, + user_name: &str, + org_name: &str, + reset_password: bool, + reset_2fa: bool, + fallback_2fa_email: bool, +) -> EmptyResult { let (subject, body_html, body_text) = get_text( - "email/admin_reset_password", + "email/admin_account_recovery", json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), "user_name": user_name, "org_name": org_name, + "reset_password": reset_password, + "reset_2fa": reset_2fa, + "fallback_2fa_email": fallback_2fa_email, }), )?; send_email(address, &subject, body_html, body_text).await diff --git a/src/static/templates/email/admin_account_recovery.hbs b/src/static/templates/email/admin_account_recovery.hbs new file mode 100644 index 00000000..a35a1d05 --- /dev/null +++ b/src/static/templates/email/admin_account_recovery.hbs @@ -0,0 +1,12 @@ +Admin account recovery from {{org_name}} organization + +{{#if reset_password}} +The master password for {{user_name}} has been changed. +{{/if}} +{{#if reset_2fa}} +Your two-step verification providers have been reset.{{#if fallback_2fa_email}} Email two factor has been activated as a fallback.{{/if}} +{{/if}} + +If you did not initiate this request, please reach out to your administrator immediately. + +{{> email/email_footer_text }} diff --git a/src/static/templates/email/admin_reset_password.html.hbs b/src/static/templates/email/admin_account_recovery.html.hbs similarity index 55% rename from src/static/templates/email/admin_reset_password.html.hbs rename to src/static/templates/email/admin_account_recovery.html.hbs index d9749d22..cf8eebed 100644 --- a/src/static/templates/email/admin_reset_password.html.hbs +++ b/src/static/templates/email/admin_account_recovery.html.hbs @@ -1,10 +1,17 @@ -Master Password Has Been Changed +Admin account recovery from {{org_name}} organization {{> email/email_header }}
- The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. + {{#if reset_password}} + The master password for {{user_name}} has been changed. + {{/if}} + {{#if reset_2fa}} + Your two-step verification providers have been reset.{{#if fallback_2fa_email}} Email two factor has been activated as a fallback.{{/if}} + {{/if}} +
+ If you did not initiate this request, please reach out to your administrator immediately.
diff --git a/src/static/templates/email/admin_reset_password.hbs b/src/static/templates/email/admin_reset_password.hbs deleted file mode 100644 index f70423f1..00000000 --- a/src/static/templates/email/admin_reset_password.hbs +++ /dev/null @@ -1,4 +0,0 @@ -Master Password Has Been Changed - -The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. -{{> email/email_footer_text }} From f1ff61300844b0664907393c0fe93f5092654784 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 8 Sep 2026 12:14:07 +0200 Subject: [PATCH 05/20] fix(security): revoke 2FA remember tokens when credentials or 2FA change (#7682) --- src/api/core/two_factor/mod.rs | 5 +++-- src/api/identity.rs | 6 ++++++ src/db/models/device.rs | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index c95fb297..0eb6563e 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -16,8 +16,8 @@ use crate::{ db::{ DbConn, DbPool, models::{ - DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor, - TwoFactorIncomplete, TwoFactorType, User, UserId, + Device, DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, + TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId, }, }, mail, @@ -151,6 +151,7 @@ async fn disable_twofactor(data: Json, headers: Headers, c if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { twofactor.delete(&conn).await?; + Device::clear_twofactor_remember_by_user(&user.uuid, &conn).await?; log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) .await; } diff --git a/src/api/identity.rs b/src/api/identity.rs index 2b1ddfb1..a2525d9b 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -905,6 +905,12 @@ async fn twofactor_auth( // Remove all twofactors from the user TwoFactor::delete_all_by_user(&user.uuid, conn).await?; + + // No device may keep skipping 2FA once every second factor is gone. + // `device` is cleared in memory too, since saving it later would restore its token. + Device::clear_twofactor_remember_by_user(&user.uuid, conn).await?; + device.delete_twofactor_remember(); + enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?; log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 6c1b686a..cc8f1cec 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -266,10 +266,22 @@ impl Device { let devices = Self::find_by_user(user_uuid, conn).await; for mut device in devices { device.refresh_token = Device::generate_refresh_token(); + device.twofactor_remember = None; device.save(false, conn).await?; } Ok(()) } + + pub async fn clear_twofactor_remember_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { + conn.run(move |conn| { + diesel::update(devices::table) + .filter(devices::user_uuid.eq(user_uuid)) + .set(devices::twofactor_remember.eq::>(None)) + .execute(conn) + .map_res("Error removing two factor remember tokens") + }) + .await + } } #[derive(Display)] From f1c36b8c1d9b2cdd0f1cf6f1c4062f81c3a70302 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 8 Sep 2026 12:14:16 +0200 Subject: [PATCH 06/20] fix(security): rate limit prelogin and auth request endpoints (#7681) --- src/api/core/accounts.rs | 16 +++++++++++----- src/api/identity.rs | 8 ++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 626f22bb..3ea6eada 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1340,11 +1340,13 @@ pub struct PreloginData { } #[post("/accounts/prelogin", data = "")] -async fn post_prelogin(data: Json, conn: DbConn) -> Json { - prelogin(data, conn).await +async fn post_prelogin(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + prelogin(data, ip, conn).await } -pub async fn prelogin(data: Json, conn: DbConn) -> Json { +pub async fn prelogin(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + crate::ratelimit::check_limit_unauthenticated(&ip.ip)?; + let data: PreloginData = data.into_inner(); let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await { @@ -1352,7 +1354,7 @@ pub async fn prelogin(data: Json, conn: DbConn) -> Json { None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None), }; - Json(json!({ + Ok(Json(json!({ "kdf": kdf_type, "kdfIterations": kdf_iter, "kdfMemory": kdf_mem, @@ -1364,7 +1366,7 @@ pub async fn prelogin(data: Json, conn: DbConn) -> Json { "parallelism": kdf_para }, "salt": null, - })) + }))) } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs @@ -1595,6 +1597,8 @@ async fn post_auth_request( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + crate::ratelimit::check_limit_unauthenticated(&client_headers.ip.ip)?; + let data = data.into_inner(); let Some(user) = User::find_by_mail(&data.email, &conn).await else { @@ -1756,6 +1760,8 @@ async fn get_auth_request_response( client_headers: ClientHeaders, conn: DbConn, ) -> JsonResult { + crate::ratelimit::check_limit_unauthenticated(&client_headers.ip.ip)?; + let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &conn).await else { err!("AuthRequest doesn't exist", "User not found") }; diff --git a/src/api/identity.rs b/src/api/identity.rs index a2525d9b..7bd12a78 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1056,13 +1056,13 @@ async fn json_err_twofactor( } #[post("/accounts/prelogin", data = "")] -async fn post_prelogin(data: Json, conn: DbConn) -> Json { - prelogin(data, conn).await +async fn post_prelogin(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + prelogin(data, ip, conn).await } #[post("/accounts/prelogin/password", data = "")] -async fn prelogin_password(data: Json, conn: DbConn) -> Json { - prelogin(data, conn).await +async fn prelogin_password(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + prelogin(data, ip, conn).await } #[post("/accounts/register", data = "")] From b7667e27bf3500a2446d39446b1a7b10b8b25991 Mon Sep 17 00:00:00 2001 From: niniconi <112842746+niniconi@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:42:58 +0800 Subject: [PATCH 07/20] fix: Correct invalid comment syntax in .dockerignore (#7274) Docker only recognizes `#` as a valid comment indicator in .dockerignore files. Using `//` causes the lines to be incorrectly parsed as glob patterns rather than comments. While this may not cause fatal errors if no matching files exist, it is syntactically invalid and could lead to unexpected behavior. Corrected the syntax to use `#`. --- .dockerignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index a9a358a3..d6ac6b9b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,7 @@ -// Ignore everything +# Ignore everything * -// Allow what is needed +# Allow what is needed !.git !docker/healthcheck.sh !docker/start.sh From de7abaaafa5ce6627e43efa52840f6df6f43da23 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 9 Sep 2026 11:51:23 +0200 Subject: [PATCH 08/20] Update Rust and adjust DockerSettings (#7690) - Update Rust to v1.98.1 which resolves a build issues with strange outcomes - Adjusted the DockerSettings and render_template to extract the `rust_version` from the `rust-toolchain.toml` file. This should prevent mismatches and forgetting to update DockerSettings. - Updated typos in GHA and Pre-Commit - Updated all possible crates including hickory which has several CVE's fixed. Signed-off-by: BlackDex --- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 357 +++++++++++++++++++---------------- Cargo.toml | 19 +- docker/DockerSettings.yaml | 3 +- docker/Dockerfile.alpine | 8 +- docker/Dockerfile.debian | 2 +- docker/render_template | 10 +- macros/Cargo.toml | 2 +- rust-toolchain.toml | 2 +- 11 files changed, 227 insertions(+), 182 deletions(-) diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 83cd581b..00fcbab4 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0 + uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 5e7100b9..31153b07 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5269c041..e8319414 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0 + rev: d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 hooks: - id: typos always_run: true diff --git a/Cargo.lock b/Cargo.lock index b8335e5b..f9c763d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +checksum = "24a8ec73eb862508b7041723c89386365894b4e7d9f6998bf1b8529e5b0ee254" dependencies = [ "compression-codecs", "compression-core", @@ -318,7 +318,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -360,9 +360,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a767267da9e2c2e189b2f9df8b5657e850ecf5352644734ba130d4a57095cf1b" +checksum = "b8d7b388a9fc3a6db15a5ec778c38b354eff1364882c94d08e0252f7a47dcaa4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -403,9 +403,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" +checksum = "ef47857a1d4488b528f4a5d5715fa7c3300820897824152234d3fa22b1426657" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -428,9 +428,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.108.0" +version = "1.109.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15301b04372832947916607983b114b3374b9db0be058a00fb7513800de1f05" +checksum = "c3cfe74df5d9ad2fedd691973ad3521ebf4f27a3c68c792556686aedb5519bab" dependencies = [ "arc-swap", "aws-credential-types", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.110.0" +version = "1.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cc2c205cb27108183cf1856333f7d584c2ba0f505421b4209ca5828f9ea899" +checksum = "81b0ec31ed6191bd11350aae4b2004198f2db21350cb0a20c57e0a92e55dd161" dependencies = [ "arc-swap", "aws-credential-types", @@ -480,9 +480,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.113.0" +version = "1.114.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68182ecb449f7537db0f4d5d25917789cf41e32074a9fe47b6a0b847fe1d2032" +checksum = "ef45745026107ec30c4ef86bd8ae4b002e7e5f6a86e4225240bdf6b06a0b944a" dependencies = [ "arc-swap", "aws-credential-types", @@ -619,9 +619,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954c563ce84507722d2679f07a35d21b9c6466b3872d513020d0281fc8112ac9" +checksum = "9c054752dd9e4dc73d0b75748c99ac2d0feafbf2f25c7b0516f03a3534161223" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -659,9 +659,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.6.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce83ce9abbb198d25bc7131e468d0f9fe1257125e58c39f3f9fc9f5098c9647" +checksum = "8f94d16e797ec62cd999fc9d5942b48fa7050c3093ddadff48e4d7528d16fcb9" dependencies = [ "base64-simd", "bytes", @@ -694,9 +694,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" +checksum = "209f3a6d82a6e9e5f94abbed94c7a26e1c052341002bf57a5fb5481f625896fc" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -895,9 +895,9 @@ dependencies = [ [[package]] name = "cached" -version = "3.1.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133b6b7d6a828c24d5055ef51e67457002b4017ae5ea3d1b1552ca35a44b1119" +checksum = "c5a6cf8262820194a1488ece477f5fb5ba9256ef25229d525470e71d6e9a5835" dependencies = [ "ahash", "async-lock", @@ -911,9 +911,9 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da80977bd46ecf98c593b651e393260f852678283989b8b7c4079304fe5e8936" +checksum = "ece0579b43cf6e927b3370c7d44359c7122e7e52b3ea635bc8484da571442edb" dependencies = [ "darling 0.20.11", "proc-macro-crate", @@ -930,9 +930,9 @@ checksum = "f5813789573ae815c8b4be58c4428e0e7ae05f0227678ba9de332ded585b9159" [[package]] name = "cc" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "jobserver", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +checksum = "100590da849306918656ffbb22576bbdfc1382b50ad10d1d50ec177d3b205fb2" dependencies = [ "brotli", "compression-core", @@ -1025,9 +1025,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" [[package]] name = "concurrent-queue" @@ -1134,6 +1134,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1191,27 +1197,27 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -1308,12 +1314,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -1346,15 +1352,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1381,13 +1387,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.23.0", + "darling_core 0.24.1", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1570,9 +1576,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.12" +version = "2.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715377c6e464cb44bb89bd8487584240516c8d5052bc645d6babc50bb8be46c3" +checksum = "e3b934ddbdcb2abb9f9fc9c30bd47bcc5618b615eea1d334cda5fdf8ff9b072a" dependencies = [ "bigdecimal", "bitflags 2.13.1", @@ -1607,9 +1613,9 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.9" +version = "2.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" +checksum = "ecbd51fb6c020672543641167efa4e6417ff7ad76849ed556ace3595e72de03a" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -1670,7 +1676,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1809,11 +1815,17 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -1908,9 +1920,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" @@ -2028,7 +2040,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -2177,7 +2189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" dependencies = [ "codemap", - "indexmap 2.14.1", + "indexmap 2.14.2", "lasso", "once_cell", "phf 0.11.3", @@ -2206,7 +2218,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.5.0", - "indexmap 2.14.1", + "indexmap 2.14.2", "slab", "tokio", "tokio-util", @@ -2299,9 +2311,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +checksum = "084e7bd6a377435d568f652153e571b50970d7ccc1d1eeec0519f834632287e1" dependencies = [ "async-trait", "cfg-if", @@ -2323,9 +2335,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +checksum = "7e2da0694c15b44c6f68a6b05e0233617008c54080e31d6eb848d858a9c5b38d" dependencies = [ "data-encoding", "idna", @@ -2343,9 +2355,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +checksum = "0e4f9f4603319422d482738f3f6fe5aac03157fdbfed1cd85a3ff45adb09072f" dependencies = [ "cfg-if", "futures-util", @@ -2483,9 +2495,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -2543,9 +2555,9 @@ dependencies = [ "http 1.5.0", "hyper 1.11.1", "hyper-util", - "rustls 0.23.43", + "rustls 0.23.44", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.5", "tower-service", ] @@ -2721,9 +2733,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.1" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -2752,9 +2764,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" dependencies = [ "serde", ] @@ -2918,9 +2930,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -2999,12 +3011,12 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.43", + "rustls 0.23.44", "rustls-native-certs", "serde", "socket2 0.6.5", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.5", "tracing", "url", ] @@ -3097,7 +3109,7 @@ name = "macros" version = "0.1.0" dependencies = [ "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3189,9 +3201,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -3237,6 +3249,33 @@ dependencies = [ "version_check", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "mysqlclient-sys" version = "0.5.2" @@ -3340,7 +3379,7 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3446,9 +3485,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33dbff14cc9bb085224256d6a81289d2f3202e85b06f408d42534b42162a4231" +checksum = "f950151f9587a51a7bed70a15fa0cff464eae96e41ae7499f97067bdafdf43eb" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3457,9 +3496,9 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dbcef97d3eb7591db2c18d5cae95c836bcce07359b98d98dd6f4e861eb77b7" +checksum = "a43405d217dfdfb543f58847336d3af672897dd1939bb7dcf314b63cf364f1c9" dependencies = [ "anyhow", "asyncband", @@ -3483,9 +3522,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ef1e1c45f3f89282a59073897e0d685e51385fed0aea771714789525cff996" +checksum = "fb9caf04d6d38713299dd4abac984b95ab16ee20e1ff09160f495c5a64644083" dependencies = [ "bytes", "log", @@ -3497,9 +3536,9 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64335f9f24ccb62ac36f1d976342b48611a75ba61979813a4f78a4ebd94de42" +checksum = "388b1d39b62535c62803754ebef89808859558697366dbedd0299345887ba461" dependencies = [ "base64 0.23.1", "bytes", @@ -3750,9 +3789,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" +checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" dependencies = [ "memchr", "ucd-trie", @@ -3760,9 +3799,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" +checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" dependencies = [ "pest", "pest_generator", @@ -3770,9 +3809,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" +checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" dependencies = [ "pest", "pest_meta", @@ -3783,9 +3822,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" +checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" dependencies = [ "pest", ] @@ -3939,9 +3978,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] @@ -4228,7 +4267,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -4348,11 +4387,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "cookie", "cookie_store", @@ -4371,7 +4410,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustls 0.23.43", + "rustls 0.23.44", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4379,7 +4418,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.5", "tokio-util", "tower", "tower-http", @@ -4453,7 +4492,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.14.1", + "indexmap 2.14.2", "log", "memchr", "multer", @@ -4485,7 +4524,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.14.1", + "indexmap 2.14.2", "proc-macro2", "quote", "rocket_http", @@ -4505,7 +4544,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.14.1", + "indexmap 2.14.2", "log", "memchr", "pear", @@ -4640,9 +4679,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "log", "once_cell", @@ -4694,7 +4733,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.43", + "rustls 0.23.44", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.15", @@ -4906,7 +4945,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -4974,16 +5013,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.1", + "indexmap 2.14.2", "jiff", "schemars 0.9.0", "schemars 1.2.2", @@ -4995,14 +5034,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -5142,9 +5181,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" [[package]] name = "socket2" @@ -5271,9 +5310,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -5389,7 +5428,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -5454,9 +5493,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" dependencies = [ "tinyvec_macros", ] @@ -5492,7 +5531,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -5507,11 +5546,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ - "rustls 0.23.43", + "rustls 0.23.44", "tokio", ] @@ -5611,7 +5650,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.14.1", + "indexmap 2.14.2", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5625,7 +5664,7 @@ version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap 2.14.1", + "indexmap 2.14.2", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.4", @@ -5901,9 +5940,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.2" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" [[package]] name = "vaultwarden" @@ -5965,7 +6004,7 @@ dependencies = [ "rocket", "rocket_ws", "rpassword", - "rustls 0.23.43", + "rustls 0.23.44", "semver", "serde", "serde_json", @@ -6040,9 +6079,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -6053,9 +6092,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -6063,9 +6102,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6073,22 +6112,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -6108,9 +6147,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -6583,18 +6622,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", @@ -6672,7 +6711,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -6689,27 +6728,27 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "bf06bd8162af0734b344780deb55b42a2429ae430870d13fcc12f238e880fe6e" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.4" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "ae42c0555055784c70058d19ba8e275528e8a99a706684868ace5da4e716a4ab" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 7d711fe1..cc6dff02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ members = ["macros"] name = "vaultwarden" version = "1.0.0" authors = ["Daniel García "] -readme = "README.md" build = "build.rs" repository.workspace = true edition.workspace = true @@ -107,7 +106,7 @@ serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" # A safe, extensible ORM and Query builder -diesel = { version = "2.3.12", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.13", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = [ @@ -125,7 +124,7 @@ libsqlite3-sys = { version = "0.38.2", optional = true } # Crypto-related libraries rand = "0.10.2" ring = "0.17.14" -rustls = { version = "0.23.43", features = ["ring", "std"], default-features = false } +rustls = { version = "0.23.44", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation @@ -183,7 +182,7 @@ email_address = "0.2.9" handlebars = { version = "6.4.4", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.13.4", default-features = false, features = [ +reqwest = { version = "0.13.5", default-features = false, features = [ # Misc "charset", "cookies", @@ -201,7 +200,7 @@ reqwest = { version = "0.13.4", default-features = false, features = [ "socks", "system-proxy", ] } -hickory-resolver = "0.26.1" +hickory-resolver = "0.26.2" # Favicon extraction libraries html5gum = "0.8.4" @@ -215,7 +214,7 @@ bytes = "1.12.1" svg-hush = "0.9.7" # Cache function results (Used for version check and favicon fetching) -cached = { version = "3.1.1", features = ["async"] } +cached = { version = "4.0.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.2" @@ -232,7 +231,7 @@ pastey = "0.2.3" governor = "0.10.4" # CIDR parsing for the trusted proxies of the client IP header -ipnet = "2.12.1" +ipnet = "2.12.2" # OIDC for SSO openidconnect = { version = "4.0.1", default-features = false } @@ -257,17 +256,17 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.58.2", default-features = false, features = ["services-fs"] } +opendal = { version = "0.59.1", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.11.0", optional = true, default-features = false, features = [ +aws-config = { version = "1.12.0", optional = true, default-features = false, features = [ "behavior-version-latest", "credentials-process", "rt-tokio", "sso", ] } aws-credential-types = { version = "1.3.0", optional = true } -aws-smithy-runtime-api = { version = "1.15.0", optional = true } +aws-smithy-runtime-api = { version = "1.16.0", optional = true } http = { version = "1.5.0", optional = true } reqsign-aws-v4 = { version = "3.3.0", optional = true } reqsign-core = { version = "3.3.1", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index fdbf40f2..6ae6e9a8 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,8 @@ vault_image_digest: "sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.98.0 # Rust version to be used +# The `rust_version` variable is extracted from `rust-toolchain.toml` +# rust_version: x.yy.z # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.24" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 491aa9e0..a91deb07 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330 ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.98.0 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.98.0 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.98.0 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.98.0 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.98.1 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.98.1 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.98.1 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.98.1 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 280559e2..b8490609 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.98.0-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.98.1-slim-trixie AS build # hadolint ignore=DL3067 COPY --from=xx / / ARG TARGETARCH diff --git a/docker/render_template b/docker/render_template index 401e0ad0..84ca8ead 100755 --- a/docker/render_template +++ b/docker/render_template @@ -3,17 +3,23 @@ import os import argparse import json +import tomllib import yaml import jinja2 # Load settings file -with open("DockerSettings.yaml", 'r') as yaml_file: +with open('DockerSettings.yaml', 'r', encoding='utf-8') as yaml_file: yaml_data = yaml.safe_load(yaml_file) +# Extract the rust_version from the rust-toolchain.toml file +script_dir = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(script_dir, '..', 'rust-toolchain.toml'), 'rb') as toolchain_file: + yaml_data["rust_version"] = tomllib.load(toolchain_file)["toolchain"]["channel"] + settings_env = jinja2.Environment( loader=jinja2.FileSystemLoader(os.getcwd()), ) -settings_yaml = yaml.safe_load(settings_env.get_template("DockerSettings.yaml").render(yaml_data)) +settings_yaml = yaml.safe_load(settings_env.get_template('DockerSettings.yaml').render(yaml_data)) args_parser = argparse.ArgumentParser() args_parser.add_argument('template_file', help='Jinja2 template file to render.') diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 84d17192..34cb913d 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] quote = "1.0.47" -syn = "3.0.4" +syn = "3.0.5" [lints] workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9bfb1d94..2be20926 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.98.0" +channel = "1.98.1" components = [ "rustfmt", "clippy" ] profile = "minimal" From 5b51b60f9407bc4e088eb1dcb035a5d395178aab Mon Sep 17 00:00:00 2001 From: Chase Douglas Date: Wed, 9 Sep 2026 05:30:25 -0700 Subject: [PATCH 09/20] Route service clients through shared HTTP setup (#7639) * storage: route OpenDAL through HTTP client OpenDAL 0.58 requires applications to provide an HTTP transport. Its default installer creates a standalone client, bypassing Vaultwarden DNS, redirect, proxy, timeout, and request configuration. Build the client through the internal HTTP interface and inject it into OpenDAL's public reqwest transport. * http: honor block setting on redirects Clients can disable host blocking for administrator-configured private services. DNS resolution honors this setting, but the redirect policy still performs config-backed host checks. Capture the setting in the redirect policy and skip those checks when blocking is disabled. This also avoids re-entering CONFIG when a remote configuration request is redirected during startup. * http: make DNS setup bootstrap-safe Remote configuration can require an HTTP client while CONFIG is still initializing. Building the DNS resolver currently reads CONFIG.dns_prefer_ipv6(), so loading an S3-backed config can deadlock. Build one resolver without consulting CONFIG. Order addresses for each lookup using the merged setting when available, falling back to the environment and then IPv4-first during bootstrap. * aws: use internal HTTP client The AWS SDK connector builds a raw reqwest client, bypassing Vaultwarden TLS, DNS, redirect, proxy, timeout, and request setup. Construct it through the internal HTTP client interface and retain the standard ten-second request deadline. Permit private AWS metadata and service endpoints by disabling non-global IP blocking. Preserve timeout errors when adapting reqwest failures to the AWS SDK so the runtime receives the correct connector error category. * Added comment for the prefer IPv function Signed-off-by: BlackDex --------- Signed-off-by: BlackDex Co-authored-by: BlackDex --- Cargo.lock | 15 ++++++++ Cargo.toml | 2 ++ src/http_client.rs | 87 ++++++++++++++++++++++++++++++++++++++-------- src/storage.rs | 19 ++++++---- 4 files changed, 102 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c763d1..55a7233b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3520,6 +3520,20 @@ dependencies = [ "web-time", ] +[[package]] +name = "opendal-http-transport-reqwest" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401999057db611e592f883fcf2cbd6754ff37af587deaadd07b8c1398b2b6b06" +dependencies = [ + "bytes", + "futures", + "http 1.5.0", + "http-body 1.1.0", + "opendal-core", + "reqwest", +] + [[package]] name = "opendal-service-fs" version = "0.59.1" @@ -5989,6 +6003,7 @@ dependencies = [ "num-derive", "num-traits", "opendal", + "opendal-http-transport-reqwest", "openidconnect", "openssl", "pastey 0.2.3", diff --git a/Cargo.toml b/Cargo.toml index cc6dff02..d3a3d5e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ vendored_openssl = ["openssl/vendored"] enable_mimalloc = ["dep:mimalloc"] s3 = [ "opendal/services-s3", + "dep:opendal-http-transport-reqwest", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", @@ -257,6 +258,7 @@ grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL opendal = { version = "0.59.1", default-features = false, features = ["services-fs"] } +opendal-http-transport-reqwest = { version = "0.59.1", default-features = false, features = ["rustls-no-provider"], optional = true } # For retrieving AWS credentials, including temporary SSO credentials aws-config = { version = "1.12.0", optional = true, default-features = false, features = [ diff --git a/src/http_client.rs b/src/http_client.rs index 0831d990..5ef293fc 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -14,7 +14,10 @@ use reqwest::{ }; use url::Host; -use crate::{CONFIG, util::is_global}; +use crate::{ + CONFIG, + util::{get_env_bool, is_global}, +}; pub fn make_http_request(method: reqwest::Method, url: &str) -> Result { static INSTANCE: LazyLock = @@ -36,7 +39,7 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder { let mut headers = header::HeaderMap::new(); headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden")); - let redirect_policy = reqwest::redirect::Policy::custom(|attempt| { + let redirect_policy = reqwest::redirect::Policy::custom(move |attempt| { if attempt.previous().len() >= 5 { return attempt.error("Too many redirects"); } @@ -45,7 +48,7 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder { return attempt.error("Invalid host"); }; - if let Err(e) = should_block_host(&host) { + if enforce_block && let Err(e) = should_block_host(&host) { return attempt.error(e); } @@ -59,6 +62,14 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder { .timeout(Duration::from_secs(10)) } +fn dns_prefer_ipv6() -> bool { + // CONFIG may require DNS to initialize, so avoid forcing it during bootstrap. + match LazyLock::get(&CONFIG) { + Some(config) => config.dns_prefer_ipv6(), + None => get_env_bool("DNS_PREFER_IPV6").unwrap_or(false), + } +} + fn should_block_ip(ip: IpAddr) -> bool { if !CONFIG.http_request_block_non_global_ips() { return false; @@ -258,12 +269,8 @@ impl CustomDnsResolver { fn new() -> Arc { TokioResolver::builder(TokioRuntimeProvider::default()) .and_then(|mut builder| { - // Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first - // This might cause issues on IPv4 only systems or containers - // Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default - if !CONFIG.dns_prefer_ipv6() { - builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6; - } + // Query both families; the preferred order is applied per lookup below. + builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6; builder.build() }) .inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) @@ -289,6 +296,17 @@ impl CustomDnsResolver { } } +fn sort_addresses(addresses: &mut [SocketAddr], prefer_ipv6: bool) { + // `sort_by_key` orders `false` before `true`. + // When IPv6 is preferred, IPv6 addresses return `false` for `is_ipv4()` and sort first. + // When IPv4 is preferred, IPv4 addresses return `false` for `is_ipv6()` and sort first. + if prefer_ipv6 { + addresses.sort_by_key(SocketAddr::is_ipv4); + } else { + addresses.sort_by_key(SocketAddr::is_ipv6); + } +} + fn pre_resolve(name: &str, enforce_block: bool) -> Result<(), CustomHttpClientError> { let Ok(host) = get_valid_host(name) else { return Err(CustomHttpClientError::Invalid { @@ -320,7 +338,9 @@ impl Resolve for CustomDns { let this = Arc::clone(&self.resolver); Box::pin(async move { let name = name.as_str(); - let results = this.resolve_domain(name, enforce_block).await?; + let mut results = this.resolve_domain(name, enforce_block).await?; + // Recheck after bootstrap so long-lived clients adopt the loaded config. + sort_addresses(&mut results, dns_prefer_ipv6()); if results.is_empty() { warn!("Unable to resolve {name} to any valid IP address"); } @@ -339,10 +359,29 @@ pub(crate) mod aws { }; use reqwest::Client; + use super::get_reqwest_client_builder; + // Adapter that wraps reqwest to be compatible with the AWS SDK #[derive(Debug)] pub(crate) struct AwsReqwestConnector { - pub(crate) client: Client, + client: Client, + } + + impl AwsReqwestConnector { + pub(crate) fn new() -> Self { + let client = get_reqwest_client_builder(false).build().expect("Failed to build AWS HTTP client"); + Self { + client, + } + } + } + + fn connector_error(error: reqwest::Error) -> ConnectorError { + if error.is_timeout() { + ConnectorError::timeout(Box::new(error)) + } else { + ConnectorError::io(Box::new(error)) + } } impl HttpConnector for AwsReqwestConnector { @@ -362,10 +401,10 @@ pub(crate) mod aws { req_builder = req_builder.body(body_bytes.to_vec()); } - let response = req_builder.send().await.map_err(|e| ConnectorError::io(Box::new(e)))?; + let response = req_builder.send().await.map_err(connector_error)?; let status = response.status().into(); - let bytes = response.bytes().await.map_err(|e| ConnectorError::io(Box::new(e)))?; + let bytes = response.bytes().await.map_err(connector_error)?; Ok(HttpResponse::new(status, bytes.into())) }; @@ -391,7 +430,7 @@ pub(crate) mod aws { mod tests { use super::*; use crate::util::is_global_hardcoded; - use std::net::Ipv4Addr; + use std::net::{Ipv4Addr, Ipv6Addr}; use url::Host; // === @@ -404,6 +443,26 @@ mod tests { } } + #[test] + fn dns_setup_does_not_initialize_config() { + assert!(LazyLock::get(&CONFIG).is_none()); + drop(CustomDns::instance(false)); + assert!(LazyLock::get(&CONFIG).is_none()); + } + + #[test] + fn dns_preference_orders_addresses() { + let ipv4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); + let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0); + let mut addresses = [ipv6, ipv4]; + + sort_addresses(&mut addresses, false); + assert_eq!(addresses, [ipv4, ipv6]); + + sort_addresses(&mut addresses, true); + assert_eq!(addresses, [ipv6, ipv4]); + } + #[test] fn dotted_decimal_loopback_normalizes() { let ip = parse_to_ip("127.0.0.1").unwrap(); diff --git a/src/storage.rs b/src/storage.rs index 689be302..32562a0d 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -77,10 +77,18 @@ pub(crate) fn operator_for_path(path: &str) -> Result = LazyLock::new(|| { + // Storage endpoints are administrator-configured and may be private. + crate::http_client::get_reqwest_client_builder(false).build().expect("Failed to build OpenDAL HTTP client") + }); + pub(super) fn is_uri(path: &str) -> bool { path.starts_with("s3://") } @@ -177,12 +185,7 @@ mod s3 { let chain = DEFAULT_CREDENTIAL_CHAIN .get_or_init(|| { - let reqwest_client = reqwest::Client::builder().build().unwrap(); - let connector = AwsReqwestConnector { - client: reqwest_client, - }; - - let conf = ProviderConfig::default().with_http_client(connector); + let conf = ProviderConfig::default().with_http_client(AwsReqwestConnector::new()); DefaultCredentialsChain::builder().configure(conf).build() }) @@ -236,7 +239,9 @@ mod s3 { builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider)); } - Ok(opendal::Operator::new(builder)?) + let http_transport = opendal::HttpTransporter::new(ReqwestTransport::new(HTTP_CLIENT.clone())); + let context = opendal::OperationContext::new().with_http_transport(http_transport); + Ok(opendal::Operator::new(builder)?.with_context(context)) } fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool { From e992cbb4f520c53d50b22fa3cc1f2f77c4a95a81 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:31:03 +0200 Subject: [PATCH 10/20] Fix iOS registration token response (#7714) * Return registration token as text/plain for Accept: */* * fix register verification response content negotiation --- src/api/identity.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 7bd12a78..6808ddde 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -3,7 +3,7 @@ use num_traits::FromPrimitive; use rocket::{ Route, form::{Form, FromForm}, - http::{Cookie, CookieJar, SameSite}, + http::{Accept, Cookie, CookieJar, MediaType, SameSite}, response::Redirect, serde::json::Json, }; @@ -1083,11 +1083,18 @@ enum RegisterVerificationResponse { #[response(status = 204)] NoContent(()), Token(Json), + PlainToken(String), +} + +// Return JSON only when the client explicitly requests it, otherwise return plain text. +fn accepts_json(accept: Option<&Accept>) -> bool { + accept.is_some_and(|accept| accept.preferred().media_type() == &MediaType::JSON) } #[post("/accounts/register/send-verification-email", data = "")] async fn register_verification_email( data: Json, + accept: Option<&Accept>, ip: ClientIp, conn: DbConn, ) -> ApiResult { @@ -1125,7 +1132,11 @@ async fn register_verification_email( } else { // If email verification is not required, return the token directly // the clients will use this token to finish the registration - Ok(RegisterVerificationResponse::Token(Json(token))) + Ok(if accepts_json(accept) { + RegisterVerificationResponse::Token(Json(token)) + } else { + RegisterVerificationResponse::PlainToken(token) + }) } } From 25dfedafd73cdeee47a50c1dfa7a451afe1ae1bd Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 9 Sep 2026 14:43:47 +0000 Subject: [PATCH 11/20] Use insert_into when possible (#6437) Co-authored-by: Timshel --- src/api/core/accounts.rs | 2 +- src/db/models/archive.rs | 9 ++- src/db/models/attachment.rs | 22 ++---- src/db/models/auth_request.rs | 30 +++---- src/db/models/cipher.rs | 22 ++---- src/db/models/collection.rs | 92 +++++++--------------- src/db/models/device.rs | 9 ++- src/db/models/emergency_access.rs | 22 ++---- src/db/models/event.rs | 27 ++++--- src/db/models/folder.rs | 33 +++----- src/db/models/group.rs | 127 +++++++++--------------------- src/db/models/organization.rs | 68 +++++----------- src/db/models/send.rs | 22 ++---- src/db/models/user.rs | 18 +++-- 14 files changed, 179 insertions(+), 324 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 3ea6eada..69be1334 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1611,7 +1611,7 @@ async fn post_auth_request( _ => err!("AuthRequest doesn't exist", "Device verification failed"), }; - let mut auth_request = AuthRequest::new( + let auth_request = AuthRequest::new( user.uuid.clone(), data.device_identifier.clone(), client_headers.device_type, diff --git a/src/db/models/archive.rs b/src/db/models/archive.rs index 83d547f2..2330fac4 100644 --- a/src/db/models/archive.rs +++ b/src/db/models/archive.rs @@ -41,17 +41,20 @@ impl Archive { ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; db_run! { conn: - sqlite, mysql { - diesel::replace_into(archives::table) + mysql { + diesel::insert_into(archives::table) .values(( archives::user_uuid.eq(user_uuid), archives::cipher_uuid.eq(cipher_uuid), archives::archived_at.eq(archived_at), )) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(archives::archived_at.eq(archived_at)) .execute(conn) .map_res("Error saving archive") } - postgresql { + postgresql, sqlite { diesel::insert_into(archives::table) .values(( archives::user_uuid.eq(user_uuid), diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 244f8c27..0536dde5 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -82,24 +82,16 @@ impl Attachment { impl Attachment { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - match diesel::replace_into(attachments::table) + mysql { + diesel::insert_into(attachments::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(attachments::table) - .filter(attachments::id.eq(&self.id)) - .set(self) - .execute(conn) - .map_res("Error saving attachment") - } - Err(e) => Err(e.into()), - }.map_res("Error saving attachment") + .map_res("Error saving attachment") } - postgresql { + postgresql, sqlite { diesel::insert_into(attachments::table) .values(self) .on_conflict(attachments::id) diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index a3876661..cc4b60fd 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -82,31 +82,23 @@ impl AuthRequest { } impl AuthRequest { - pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { + pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - match diesel::replace_into(auth_requests::table) - .values(&*self) + mysql { + diesel::insert_into(auth_requests::table) + .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(auth_requests::table) - .filter(auth_requests::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error auth_request") - } - Err(e) => Err(e.into()), - }.map_res("Error auth_request") + .map_res("Error saving auth_request") } - postgresql { + postgresql, sqlite { diesel::insert_into(auth_requests::table) - .values(&*self) + .values(self) .on_conflict(auth_requests::uuid) .do_update() - .set(&*self) + .set(self) .execute(conn) .map_res("Error saving auth_request") } diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index eed5041d..721d9790 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -440,24 +440,16 @@ impl Cipher { self.updated_at = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(ciphers::table) + mysql { + diesel::insert_into(ciphers::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(ciphers::table) - .filter(ciphers::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving cipher") - } - Err(e) => Err(e.into()), - }.map_res("Error saving cipher") + .map_res("Error saving cipher") } - postgresql { + postgresql, sqlite { diesel::insert_into(ciphers::table) .values(&*self) .on_conflict(ciphers::uuid) diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 8aec90ea..be108f13 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -168,24 +168,16 @@ impl Collection { self.update_users_revision(conn).await; db_run! { conn: - sqlite, mysql { - match diesel::replace_into(collections::table) + mysql { + diesel::insert_into(collections::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(collections::table) - .filter(collections::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error saving collection") - } - Err(e) => Err(e.into()), - }.map_res("Error saving collection") + .map_res("Error saving collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(collections::table) .values(self) .on_conflict(collections::uuid) @@ -728,53 +720,30 @@ impl CollectionUser { ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; + let values = ( + users_collections::user_uuid.eq(user_uuid), + users_collections::collection_uuid.eq(collection_uuid), + users_collections::read_only.eq(read_only), + users_collections::hide_passwords.eq(hide_passwords), + users_collections::manage.eq(manage), + ); + db_run! { conn: - sqlite, mysql { - match diesel::replace_into(users_collections::table) - .values(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) + mysql { + diesel::insert_into(users_collections::table) + .values(values) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(values) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(users_collections::table) - .filter(users_collections::user_uuid.eq(user_uuid)) - .filter(users_collections::collection_uuid.eq(collection_uuid)) - .set(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) - .execute(conn) - .map_res("Error adding user to collection") - } - Err(e) => Err(e.into()), - }.map_res("Error adding user to collection") + .map_res("Error adding user to collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(users_collections::table) - .values(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) + .values(values) .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) .do_update() - .set(( - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) + .set(values) .execute(conn) .map_res("Error adding user to collection") } @@ -909,19 +878,18 @@ impl CollectionCipher { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: - sqlite, mysql { - // Not checking for ForeignKey Constraints here. - // Table ciphers_collections does not have ForeignKey Constraints which would cause conflicts. - // This table has no constraints pointing to itself, but only to others. - diesel::replace_into(ciphers_collections::table) + mysql { + diesel::insert_into(ciphers_collections::table) .values(( ciphers_collections::cipher_uuid.eq(cipher_uuid), ciphers_collections::collection_uuid.eq(collection_uuid), )) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) .map_res("Error adding cipher to collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(ciphers_collections::table) .values(( ciphers_collections::cipher_uuid.eq(cipher_uuid), diff --git a/src/db/models/device.rs b/src/db/models/device.rs index cc8f1cec..5e5f1f97 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -146,15 +146,18 @@ impl Device { } db_run! { conn: - sqlite, mysql { + mysql { crate::util::retry(|| - diesel::replace_into(devices::table) + diesel::insert_into(devices::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn), 10, ).map_res("Error saving device") } - postgresql { + postgresql, sqlite { crate::util::retry(|| diesel::insert_into(devices::table) .values(&*self) diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 45fad91f..09783061 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -146,24 +146,16 @@ impl EmergencyAccess { self.updated_at = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(emergency_access::table) + mysql { + diesel::insert_into(emergency_access::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(emergency_access::table) - .filter(emergency_access::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error updating emergency access") - } - Err(e) => Err(e.into()), - }.map_res("Error saving emergency access") + .map_res("Error saving emergency access") } - postgresql { + postgresql, sqlite { diesel::insert_into(emergency_access::table) .values(&*self) .on_conflict(emergency_access::uuid) diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 1f307979..2d9ed8b2 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -208,20 +208,23 @@ impl Event { /// Basic Queries pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - diesel::replace_into(event::table) - .values(self) - .execute(conn) - .map_res("Error saving event") + mysql { + diesel::insert_into(event::table) + .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) + .execute(conn) + .map_res("Error saving event") } - postgresql { + postgresql, sqlite { diesel::insert_into(event::table) - .values(self) - .on_conflict(event::uuid) - .do_update() - .set(self) - .execute(conn) - .map_res("Error saving event") + .values(self) + .on_conflict(event::uuid) + .do_update() + .set(self) + .execute(conn) + .map_res("Error saving event") } } } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 745608e3..adbe993f 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -77,24 +77,16 @@ impl Folder { self.updated_at = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(folders::table) + mysql { + diesel::insert_into(folders::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(folders::table) - .filter(folders::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving folder") - } - Err(e) => Err(e.into()), - }.map_res("Error saving folder") + .map_res("Error saving folder") } - postgresql { + postgresql, sqlite { diesel::insert_into(folders::table) .values(&*self) .on_conflict(folders::uuid) @@ -147,16 +139,15 @@ impl Folder { impl FolderCipher { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - // Not checking for ForeignKey Constraints here. - // Table folders_ciphers does not have ForeignKey Constraints which would cause conflicts. - // This table has no constraints pointing to itself, but only to others. - diesel::replace_into(folders_ciphers::table) + mysql { + diesel::insert_into(folders_ciphers::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) .map_res("Error adding cipher to folder") } - postgresql { + postgresql, sqlite { diesel::insert_into(folders_ciphers::table) .values(self) .on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid)) diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 37037de6..32e9333f 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -166,24 +166,16 @@ impl Group { self.revision_date = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(groups::table) + mysql { + diesel::insert_into(groups::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(groups::table) - .filter(groups::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving group") - } - Err(e) => Err(e.into()), - }.map_res("Error saving group") + .map_res("Error saving group") } - postgresql { + postgresql, sqlite { diesel::insert_into(groups::table) .values(&*self) .on_conflict(groups::uuid) @@ -326,53 +318,30 @@ impl CollectionGroup { group_user.update_user_revision(conn).await; } + let values = ( + collections_groups::collections_uuid.eq(&self.collections_uuid), + collections_groups::groups_uuid.eq(&self.groups_uuid), + collections_groups::read_only.eq(&self.read_only), + collections_groups::hide_passwords.eq(&self.hide_passwords), + collections_groups::manage.eq(&self.manage), + ); + db_run! { conn: - sqlite, mysql { - match diesel::replace_into(collections_groups::table) - .values(( - collections_groups::collections_uuid.eq(&self.collections_uuid), - collections_groups::groups_uuid.eq(&self.groups_uuid), - collections_groups::read_only.eq(&self.read_only), - collections_groups::hide_passwords.eq(&self.hide_passwords), - collections_groups::manage.eq(&self.manage), - )) + mysql { + diesel::insert_into(collections_groups::table) + .values(values) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(values) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(collections_groups::table) - .filter(collections_groups::collections_uuid.eq(&self.collections_uuid)) - .filter(collections_groups::groups_uuid.eq(&self.groups_uuid)) - .set(( - collections_groups::collections_uuid.eq(&self.collections_uuid), - collections_groups::groups_uuid.eq(&self.groups_uuid), - collections_groups::read_only.eq(&self.read_only), - collections_groups::hide_passwords.eq(&self.hide_passwords), - collections_groups::manage.eq(&self.manage), - )) - .execute(conn) - .map_res("Error adding group to collection") - } - Err(e) => Err(e.into()), - }.map_res("Error adding group to collection") + .map_res("Error adding group to collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(collections_groups::table) - .values(( - collections_groups::collections_uuid.eq(&self.collections_uuid), - collections_groups::groups_uuid.eq(&self.groups_uuid), - collections_groups::read_only.eq(self.read_only), - collections_groups::hide_passwords.eq(self.hide_passwords), - collections_groups::manage.eq(self.manage), - )) + .values(values) .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid)) .do_update() - .set(( - collections_groups::read_only.eq(self.read_only), - collections_groups::hide_passwords.eq(self.hide_passwords), - collections_groups::manage.eq(self.manage), - )) + .set(values) .execute(conn) .map_res("Error adding group to collection") } @@ -497,43 +466,25 @@ impl GroupUser { pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { self.update_user_revision(conn).await; + let values = ( + groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), + groups_users::groups_uuid.eq(&self.groups_uuid), + ); + db_run! { conn: - sqlite, mysql { - match diesel::replace_into(groups_users::table) - .values(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) + mysql { + diesel::insert_into(groups_users::table) + .values(values) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(groups_users::table) - .filter(groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid)) - .filter(groups_users::groups_uuid.eq(&self.groups_uuid)) - .set(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) - .execute(conn) - .map_res("Error adding user to group") - } - Err(e) => Err(e.into()), - }.map_res("Error adding user to group") + .map_res("Error adding user to group") } - postgresql { + postgresql, sqlite { diesel::insert_into(groups_users::table) - .values(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) + .values(values) .on_conflict((groups_users::users_organizations_uuid, groups_users::groups_uuid)) - .do_update() - .set(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) + .do_nothing() .execute(conn) .map_res("Error adding user to group") } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index bdb69864..29016865 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -353,25 +353,16 @@ impl Organization { } db_run! { conn: - sqlite, mysql { - match diesel::replace_into(organizations::table) + mysql { + diesel::insert_into(organizations::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(organizations::table) - .filter(organizations::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error saving organization") - } - Err(e) => Err(e.into()), - }.map_res("Error saving organization") - + .map_res("Error saving organization") } - postgresql { + postgresql, sqlite { diesel::insert_into(organizations::table) .values(self) .on_conflict(organizations::uuid) @@ -753,24 +744,16 @@ impl Membership { User::update_uuid_revision(&self.user_uuid, conn).await; db_run! { conn: - sqlite, mysql { - match diesel::replace_into(users_organizations::table) + mysql { + diesel::insert_into(users_organizations::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(users_organizations::table) - .filter(users_organizations::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error adding user to organization") - }, - Err(e) => Err(e.into()), - }.map_res("Error adding user to organization") + .map_res("Error adding user to organization") } - postgresql { + postgresql, sqlite { diesel::insert_into(users_organizations::table) .values(self) .on_conflict(users_organizations::uuid) @@ -1186,25 +1169,16 @@ impl Membership { impl OrganizationApiKey { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - match diesel::replace_into(organization_api_key::table) + mysql { + diesel::insert_into(organization_api_key::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(organization_api_key::table) - .filter(organization_api_key::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error saving organization") - } - Err(e) => Err(e.into()), - }.map_res("Error saving organization") - + .map_res("Error saving organization") } - postgresql { + postgresql, sqlite { diesel::insert_into(organization_api_key::table) .values(self) .on_conflict((organization_api_key::uuid, organization_api_key::org_uuid)) diff --git a/src/db/models/send.rs b/src/db/models/send.rs index c5bc98c4..d7de7749 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -202,24 +202,16 @@ impl Send { self.revision_date = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(sends::table) + mysql { + diesel::insert_into(sends::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(sends::table) - .filter(sends::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving send") - } - Err(e) => Err(e.into()), - }.map_res("Error saving send") + .map_res("Error saving send") } - postgresql { + postgresql, sqlite { diesel::insert_into(sends::table) .values(&*self) .on_conflict(sends::uuid) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 93d750d5..81cb8d84 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -463,15 +463,17 @@ impl Invitation { } db_run! { conn: - sqlite, mysql { - // Not checking for ForeignKey Constraints here - // Table invitations does not have any ForeignKey Constraints. - diesel::replace_into(invitations::table) + // Not checking for ForeignKey Constraints here + // Table invitations does not have any ForeignKey Constraints. + mysql { + diesel::insert_into(invitations::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) .map_res("Error saving invitation") } - postgresql { + postgresql, sqlite { diesel::insert_into(invitations::table) .values(self) .on_conflict(invitations::email) @@ -528,13 +530,13 @@ pub struct UserId(String); impl SsoUser { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - diesel::replace_into(sso_users::table) + mysql { + diesel::insert_into(sso_users::table) .values(self) .execute(conn) .map_res("Error saving SSO user") } - postgresql { + postgresql, sqlite { diesel::insert_into(sso_users::table) .values(self) .execute(conn) From eb212e23fad88e6136723f43e5b73543fa7026d3 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 9 Sep 2026 18:24:33 +0200 Subject: [PATCH 12/20] Fix archiveDate update (#7722) When `archiveDate` is set to `null` it should unarchive it for that specific user, which is what Bitwarden does. This should fix this by checking and validating if it is `null` Fixes #7581 Signed-off-by: BlackDex --- src/api/core/ciphers.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 13021ca3..50be6732 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -537,11 +537,12 @@ pub async fn update_cipher_from_data( cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?; - if let Some(dt_str) = data.archived_date { - match NaiveDateTime::parse_from_str(&dt_str, "%+") { + match data.archived_date { + Some(dt_str) => match NaiveDateTime::parse_from_str(&dt_str, "%+") { Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?, Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"), - } + }, + None => cipher.unarchive(&headers.user.uuid, conn).await?, } if ut != UpdateType::None { From fddc8e71be5904333acd135734cd1e8f4d8f9c20 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:52:49 +0200 Subject: [PATCH 13/20] Support the vault banner policy (#7748) --- src/api/core/events.rs | 20 ++++++++++++++++++++ src/db/models/event.rs | 2 +- src/db/models/org_policy.rs | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 2c437a36..a5b5b6b1 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -199,6 +199,26 @@ async fn post_events_collect(data: Json>, headers: Headers, .await; } } + // Only the vault notification banner click is accepted from clients. The rest of + // the 1500..=1599 range is written server-side and must not be forgeable by a client. + t if t == EventType::OrganizationUserNotificationBannerActionClicked as i32 => { + if let Some(org_id) = &event.organization_id + && let Some(membership) = + Membership::find_confirmed_by_user_and_org(&headers.user.uuid, org_id, &conn).await + { + log_event_impl( + event.r#type, + &membership.uuid, + org_id, + &headers.user.uuid, + headers.device.atype, + Some(event_date), + &headers.ip.ip, + &conn, + ) + .await; + } + } _ => { // The cipher determines the organization the event is logged to, so make sure the // user can actually access it instead of trusting the provided cipher uuid. diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 2d9ed8b2..cc0eb504 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -113,7 +113,7 @@ pub enum EventType { OrganizationUserAdminResetTwoFactor = 1519, // OrganizationUserRevoked_TwoFactorNonCompliance = 1520, // OrganizationUserRevoked_SingleOrganizationNonCompliance = 1521, - // OrganizationUserNotificationBannerActionClicked = 1522, + OrganizationUserNotificationBannerActionClicked = 1522, // Organization OrganizationUpdated = 1600, diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 2b45cd86..bf927d5f 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -49,6 +49,7 @@ pub enum OrgPolicyType { // AutotypeDefaultSetting = 17, // Not supported yet // AutoConfirm = 18, // Not supported (not implemented yet) // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed) + OrganizationUserNotification = 20, } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5 From 8719f86fcf7a58a145eb89ae10987ac11f3135b1 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:52:57 +0200 Subject: [PATCH 14/20] Add basic auth response client feature flag (#7745) --- .env.template | 1 + src/config.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index d22145b8..73226055 100644 --- a/.env.template +++ b/.env.template @@ -406,6 +406,7 @@ ## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "enable-basic-auth-response": Enable HTTP Basic Auth autofill in the browser extension (Browser >= 2026.9.0) # EXPERIMENTAL_CLIENT_FEATURE_FLAGS= ## Require new device emails. When a user logs in an email is required to be sent. diff --git a/src/config.rs b/src/config.rs index 37fc3e85..18cc6504 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1427,6 +1427,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "pm-5594-safari-account-switching", "pm-32413-multi-client-password-management", // Autofill Team + "enable-basic-auth-response", "ssh-agent", "ssh-agent-v2", // Key Management Team From 9c8aa2359ff3e38f5b179b68bba34baeb9d72778 Mon Sep 17 00:00:00 2001 From: Timshel Date: Fri, 18 Sep 2026 14:53:06 +0000 Subject: [PATCH 15/20] User key id (#7693) Co-authored-by: Timshel --- .../2026-09-02-120000_add_key_id/down.sql | 0 .../2026-09-02-120000_add_key_id/up.sql | 1 + .../2026-09-02-120000_add_key_id/down.sql | 0 .../mysql/2026-09-02-120000_add_key_id/up.sql | 1 + .../2026-09-02-120000_add_key_id/down.sql | 0 .../2026-09-02-120000_add_key_id/up.sql | 1 + .../2026-09-02-120000_add_key_id/down.sql | 0 .../2026-09-02-120000_add_key_id/up.sql | 1 + src/api/core/accounts.rs | 23 +++++++++++++++-- src/api/core/ciphers.rs | 25 +++++++++++++++++-- src/db/models/mod.rs | 2 +- src/db/models/user.rs | 24 ++++++++++++++++++ src/db/schema.rs | 1 + 13 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 migrations/cockroachdb/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql create mode 100644 migrations/mysql/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/mysql/2026-09-02-120000_add_key_id/up.sql create mode 100644 migrations/postgresql/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/postgresql/2026-09-02-120000_add_key_id/up.sql create mode 100644 migrations/sqlite/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/sqlite/2026-09-02-120000_add_key_id/up.sql diff --git a/migrations/cockroachdb/2026-09-02-120000_add_key_id/down.sql b/migrations/cockroachdb/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql b/migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/migrations/mysql/2026-09-02-120000_add_key_id/down.sql b/migrations/mysql/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2026-09-02-120000_add_key_id/up.sql b/migrations/mysql/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/mysql/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/migrations/postgresql/2026-09-02-120000_add_key_id/down.sql b/migrations/postgresql/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2026-09-02-120000_add_key_id/up.sql b/migrations/postgresql/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/postgresql/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/migrations/sqlite/2026-09-02-120000_add_key_id/down.sql b/migrations/sqlite/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2026-09-02-120000_add_key_id/up.sql b/migrations/sqlite/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/sqlite/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 69be1334..8cc5e55b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -21,8 +21,9 @@ use crate::{ DbConn, DbPool, models::{ AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest, - EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, - OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType, + EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, KeyId, Membership, + MembershipId, OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, + UserKdfType, }, }, mail, @@ -46,6 +47,7 @@ pub fn routes() -> Vec { post_set_password, post_kdf, post_rotatekey, + post_user_key, post_sstamp, post_email_token, post_email, @@ -1025,6 +1027,23 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: save_result } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct KeyIdData { + user_key_id: KeyId, +} + +#[post("/accounts/key-management/user-key-id", data = "")] +async fn post_user_key(data: Json, headers: Headers, conn: DbConn) -> EmptyResult { + let mut user = headers.user; + if user.key_id.is_some() { + err_code!("Unexpected data", Status::UnprocessableEntity.code); + } + + user.key_id = Some(data.into_inner().user_key_id); + user.save(&conn).await +} + #[post("/accounts/security-stamp", data = "")] async fn post_sstamp(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { let data: PasswordOrOtpData = data.into_inner(); diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 50be6732..d8081cba 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -6,6 +6,7 @@ use rocket::{ Route, form::{Form, FromForm}, fs::TempFile, + http::Status, serde::json::Json, }; use serde_json::Value; @@ -21,8 +22,8 @@ use crate::{ DbConn, DbPool, models::{ Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, - CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, - MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, + CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, KeyId, + Membership, MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, }, }, util::{NumberOrString, deser_opt_nonempty_str, save_temp_file}, @@ -198,6 +199,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option, + pub encrypted_for: UserId, // Added in web-v2025.6.0 + // Added in web-v2025.8.1, Optional for compat + pub encrypted_by_key_id: Option, + /* Login = 1, SecureNote = 2, @@ -333,6 +339,10 @@ async fn post_ciphers_create( ) -> JsonResult { let mut data: ShareCipherData = data.into_inner(); + if data.cipher.encrypted_for != headers.user.uuid { + err_code!("Invalid user cipher", Status::UnprocessableEntity.code); + } + // This check is usually only needed in update_cipher_from_data(), but we // need it here as well to avoid creating an empty cipher in the call to // cipher.save() below. @@ -362,6 +372,17 @@ async fn post_ciphers_create( async fn post_ciphers(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { let mut data: CipherData = data.into_inner(); + if data.encrypted_for != headers.user.uuid { + err_code!("Invalid user cipher", Status::UnprocessableEntity.code); + } + + if let Some(cipher_key_id) = &data.encrypted_by_key_id + && let Some(user_key_id) = &headers.user.key_id + && cipher_key_id != user_key_id + { + err_code!("Invalid key cipher", Status::UnprocessableEntity.code); + } + // The web/browser clients set this field to null as expected, but the // mobile clients seem to set the invalid value `0001-01-01T00:00:00`, // which results in a warning message being logged. This field isn't diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 0ed8ef91..0e4073a5 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -39,4 +39,4 @@ pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeResponseError, SsoAuth}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; -pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException}; +pub use self::user::{Invitation, KeyId, SsoUser, User, UserId, UserKdfType, UserStampException}; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 81cb8d84..36089d1a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -69,6 +69,8 @@ pub struct User { pub avatar_color: Option, pub external_id: Option, // Todo: Needs to be removed in the future, this is not used anymore. + + pub key_id: Option, } #[derive(Identifiable, Queryable, Insertable)] @@ -154,6 +156,8 @@ impl User { avatar_color: None, external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. + + key_id: None, } } @@ -527,6 +531,26 @@ impl Invitation { #[from(forward)] pub struct UserId(String); +#[derive( + Clone, + Debug, + DieselNewType, + FromForm, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + AsRef, + Deref, + Display, + From, + UuidFromParam, +)] +#[deref(forward)] +#[from(forward)] +pub struct KeyId(String); + impl SsoUser { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: diff --git a/src/db/schema.rs b/src/db/schema.rs index af342186..98b1eda6 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -217,6 +217,7 @@ table! { api_key -> Nullable, avatar_color -> Nullable, external_id -> Nullable, + key_id -> Nullable, } } From c3f5477525aed713ed340358d3ca982309979032 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:53:17 +0200 Subject: [PATCH 16/20] Add accepted organization data to sync response (#7666) --- src/api/core/ciphers.rs | 7 +++++++ src/db/models/org_policy.rs | 20 ++++++++++++++++++++ src/db/models/organization.rs | 15 +++++++++++++++ src/db/models/user.rs | 6 ++++++ 4 files changed, 48 insertions(+) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index d8081cba..af79a9ca 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -162,6 +162,12 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option = OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect(); + let policies_new_json: Vec = OrgPolicy::find_accepted_and_confirmed_by_user(&headers.user.uuid, &conn) + .await + .iter() + .map(OrgPolicy::to_json) + .collect(); + let domains_json = if data.exclude_domains { Value::Null } else { @@ -194,6 +200,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option Vec { + conn.run(move |conn| { + org_policies::table + .inner_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), + ) + .filter( + users_organizations::status + .eq(MembershipStatus::Accepted as i32) + .or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)), + ) + .select(org_policies::all_columns) + .load::(conn) + .expect("Error loading org_policy") + }) + .await + } + pub async fn find_by_org_and_type( org_uuid: &OrganizationId, policy_type: OrgPolicyType, diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 29016865..d615a3fc 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -844,6 +844,21 @@ impl Membership { .await } + pub async fn find_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { + conn.run(move |conn| { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter( + users_organizations::status + .eq(MembershipStatus::Accepted as i32) + .or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)), + ) + .load::(conn) + .unwrap_or_default() + }) + .await + } + pub async fn find_invited_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { conn.run(move |conn| { users_organizations::table diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 36089d1a..3412b142 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -263,6 +263,11 @@ impl User { orgs_json.push(c.to_json(conn).await); } + let mut orgs_new_json = Vec::new(); + for c in Membership::find_accepted_and_confirmed_by_user(&self.uuid, conn).await { + orgs_new_json.push(c.to_json(conn).await); + } + let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).await.is_empty(); // TODO: Might want to save the status field in the DB @@ -303,6 +308,7 @@ impl User { "privateKey": self.private_key, "securityStamp": self.security_stamp, "organizations": orgs_json, + "organizationsNew": orgs_new_json, "providers": [], "providerOrganizations": [], "forcePasswordReset": false, From 64c56411f6a16ac4466c12199587101566cf7efa Mon Sep 17 00:00:00 2001 From: "Berk D. Demir" <11135+bdd@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:53:27 -0700 Subject: [PATCH 17/20] Add `pm-32009-new-item-types` feature flag (#7478) Introduced in 2026.4.0 for all clients, enables new item types bank account, driver's license, and passport. --- .env.template | 1 + src/api/core/ciphers.rs | 9 +++++++++ src/config.rs | 2 ++ 3 files changed, 12 insertions(+) diff --git a/.env.template b/.env.template index 73226055..8ed6c5c2 100644 --- a/.env.template +++ b/.env.template @@ -402,6 +402,7 @@ ## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >= 2025.9.2) ## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2) ## - "pm-30529-webauthn-related-origins": +## - "pm-32009-new-item-types": Enable new item types: Bank Account, Driver's License, and Passport (Clients >= 2026.4.0) ## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index af79a9ca..a5b7e58b 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -279,6 +279,9 @@ pub struct CipherData { Card = 3, Identity = 4, SshKey = 5 + BankAccount = 6 + DriversLicense = 7 + Passport = 8 */ pub r#type: i32, pub name: String, @@ -291,6 +294,9 @@ pub struct CipherData { card: Option, identity: Option, ssh_key: Option, + bank_account: Option, + drivers_license: Option, + passport: Option, favorite: Option, reprompt: Option, @@ -538,6 +544,9 @@ pub async fn update_cipher_from_data( 3 => data.card, 4 => data.identity, 5 => data.ssh_key, + 6 => data.bank_account, + 7 => data.drivers_license, + 8 => data.passport, _ => err!("Invalid type"), }; diff --git a/src/config.rs b/src/config.rs index 18cc6504..0bb22f7f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1442,6 +1442,8 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "cxp-export-mobile", // Platform Team "pm-30529-webauthn-related-origins", + // Vault Team + "pm-32009-new-item-types", ]; impl Config { From a24683636a448949b0bf535fb2c88ec7f3814b2c Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Fri, 18 Sep 2026 18:44:50 +0200 Subject: [PATCH 18/20] Update Crates, GHA and JS (#7751) Signed-off-by: BlackDex --- .github/workflows/hadolint.yml | 2 +- .github/workflows/release.yml | 6 +- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 271 +- Cargo.toml | 14 +- macros/Cargo.toml | 2 +- src/static/scripts/datatables.css | 16 +- src/static/scripts/datatables.js | 9864 +++++++++++++++-------------- 10 files changed, 5101 insertions(+), 5080 deletions(-) diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index a429d10f..c4b83aa6 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -20,7 +20,7 @@ jobs: steps: # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 311891b7..903a5e22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,13 +58,13 @@ jobs: steps: - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -185,7 +185,7 @@ jobs: - name: Bake ${{ matrix.base_image }} containers id: bake_vw - uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0 + uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 env: BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 7f41885d..f0fbf192 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 00fcbab4..3c880e91 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 + uses: crate-ci/typos@512fc24f32f44ab01972217aaaf3dc86ec234d53 # v1.50.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8319414..d2a509e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 + rev: 512fc24f32f44ab01972217aaaf3dc86ec234d53 # v1.50.2 hooks: - id: typos always_run: true diff --git a/Cargo.lock b/Cargo.lock index 55a7233b..2ccd4b1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure", + "synstructure 0.13.2", ] [[package]] @@ -151,9 +151,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.45" +version = "0.4.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a8ec73eb862508b7041723c89386365894b4e7d9f6998bf1b8529e5b0ee254" +checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" dependencies = [ "compression-codecs", "compression-core", @@ -318,18 +318,14 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "asyncband" -version = "0.6.7" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0" -dependencies = [ - "hashbrown 0.17.1", - "slab", -] +checksum = "f2d85fd3d291fabcc40c7232c92c280ec1754fd7b5d7ea769f143222143e179a" [[package]] name = "atomic" @@ -403,9 +399,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.9.2" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef47857a1d4488b528f4a5d5715fa7c3300820897824152234d3fa22b1426657" +checksum = "25b43ad47adc2517efe3d706559d94b97e50e80e0321b3cadbc9f77cee88adcd" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -428,9 +424,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.109.0" +version = "1.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cfe74df5d9ad2fedd691973ad3521ebf4f27a3c68c792556686aedb5519bab" +checksum = "65700d75f6da78c89a61a49f1539979400a4609d3329376b1dc2133d7a1b2b90" dependencies = [ "arc-swap", "aws-credential-types", @@ -454,9 +450,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.111.0" +version = "1.113.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b0ec31ed6191bd11350aae4b2004198f2db21350cb0a20c57e0a92e55dd161" +checksum = "eb1147c254af7b6cee3ace90ae7da6f21366ce9a9b899035fa8a0fdf586eb75a" dependencies = [ "arc-swap", "aws-credential-types", @@ -480,9 +476,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.114.0" +version = "1.116.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef45745026107ec30c4ef86bd8ae4b002e7e5f6a86e4225240bdf6b06a0b944a" +checksum = "035c7392e751d20bf1060d625d023587eb14099646110a9cff7c697bfe4b253a" dependencies = [ "arc-swap", "aws-credential-types", @@ -507,9 +503,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +checksum = "31d955e76ff96acd555bf06fa0fa6d5bf9335fa84ae7c64481b20ae61d231f70" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -561,9 +557,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.63.0" +version = "0.63.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" +checksum = "3385d469edbe8b60cc72002784652b5efca39178192aa9cc4b44c9875c6bdc18" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -581,9 +577,9 @@ dependencies = [ [[package]] name = "aws-smithy-query" -version = "0.62.0" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" +checksum = "f1d1d71f6562be974caa85442ecd90194c40fdb5df045f182a6c2e872ce95056" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -594,9 +590,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.14.0" +version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b82e438d30e02a825d363bd639a9efaed68a8089d86101054b0081e7e0d3e606" +checksum = "3296253d3a91b3f938a3f2bcce4daebadcb4aa4228153fcec90f9d23532b4484" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -619,9 +615,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.16.0" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c054752dd9e4dc73d0b75748c99ac2d0feafbf2f25c7b0516f03a3534161223" +checksum = "6d881a7b7ad179fd6611680c9de89f716fb00ab40299a9a7b8c6913e8f7511a8" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -648,9 +644,9 @@ dependencies = [ [[package]] name = "aws-smithy-schema" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +checksum = "e8f395d93304280b64b7632fea798d177e74897fe7f063416ce627cd6fa24829" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -659,9 +655,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f94d16e797ec62cd999fc9d5942b48fa7050c3093ddadff48e4d7528d16fcb9" +checksum = "0b791f3ac597193fe1d08b82366986eb1f5bc31f2ac6c194c0855276116c76cd" dependencies = [ "base64-simd", "bytes", @@ -682,9 +678,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.62.0" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" +checksum = "0b932c8d6dc127fc980eecd78f8694ae9b9551b69a93a7def2a199c1c0033daf" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -785,9 +781,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blake2" @@ -930,9 +926,9 @@ checksum = "f5813789573ae815c8b4be58c4428e0e7ae05f0227678ba9de332ded585b9159" [[package]] name = "cc" -version = "1.4.5" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ "find-msvc-tools", "jobserver", @@ -942,9 +938,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -1011,9 +1007,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100590da849306918656ffbb22576bbdfc1382b50ad10d1d50ec177d3b205fb2" +checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" dependencies = [ "brotli", "compression-core", @@ -1170,9 +1166,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -1360,7 +1356,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1393,7 +1389,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core 0.24.1", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1567,7 +1563,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -1581,7 +1577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b934ddbdcb2abb9f9fc9c30bd47bcc5618b615eea1d334cda5fdf8ff9b072a" dependencies = [ "bigdecimal", - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "chrono", "diesel_derives", @@ -1676,7 +1672,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1920,9 +1916,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" [[package]] name = "flate2" @@ -2040,7 +2036,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2311,9 +2307,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e7bd6a377435d568f652153e571b50970d7ccc1d1eeec0519f834632287e1" +checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" dependencies = [ "async-trait", "cfg-if", @@ -2335,9 +2331,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2da0694c15b44c6f68a6b05e0233617008c54080e31d6eb848d858a9c5b38d" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" dependencies = [ "data-encoding", "idna", @@ -2355,9 +2351,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4f9f4603319422d482738f3f6fe5aac03157fdbfed1cd85a3ff45adb09072f" +checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" dependencies = [ "cfg-if", "futures-util", @@ -2555,7 +2551,7 @@ dependencies = [ "http 1.5.0", "hyper 1.11.1", "hyper-util", - "rustls 0.23.44", + "rustls 0.23.45", "tokio", "tokio-rustls 0.26.5", "tower-service", @@ -2805,9 +2801,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ "defmt", "jiff-core", @@ -2824,18 +2820,19 @@ dependencies = [ [[package]] name = "jiff-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" dependencies = [ "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ "jiff-core", "proc-macro2", @@ -2941,9 +2938,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "11.0.0" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65" +checksum = "e75fe14a82d81e5f5af639997db37d8b96045938a7ac6ab18cdbe1c7467e05e1" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -3011,7 +3008,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.44", + "rustls 0.23.45", "rustls-native-certs", "serde", "socket2 0.6.5", @@ -3109,7 +3106,7 @@ name = "macros" version = "0.1.0" dependencies = [ "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -3267,7 +3264,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -3299,7 +3296,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -3379,7 +3376,7 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -3485,9 +3482,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950151f9587a51a7bed70a15fa0cff464eae96e41ae7499f97067bdafdf43eb" +checksum = "9fe43e16d96bed57937eb7c4a28559bf4c5fc4a3951656505d1fdd4f856349b4" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3496,9 +3493,9 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a43405d217dfdfb543f58847336d3af672897dd1939bb7dcf314b63cf364f1c9" +checksum = "de4566a412776e3f65d53dd9fceced5b432a0a9c9a1e8d50ccf58823795e5c5b" dependencies = [ "anyhow", "asyncband", @@ -3522,9 +3519,9 @@ dependencies = [ [[package]] name = "opendal-http-transport-reqwest" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401999057db611e592f883fcf2cbd6754ff37af587deaadd07b8c1398b2b6b06" +checksum = "f772ce6c137ab43647726116d505a649a88d41928fcd813438eb30b02312bcbb" dependencies = [ "bytes", "futures", @@ -3536,9 +3533,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9caf04d6d38713299dd4abac984b95ab16ee20e1ff09160f495c5a64644083" +checksum = "a0e7d8974aebd33be899200c9d0d50b2141fd5fa5c462b29e20653e3ddbfe339" dependencies = [ "bytes", "log", @@ -3550,9 +3547,9 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388b1d39b62535c62803754ebef89808859558697366dbedd0299345887ba461" +checksum = "2942f2d8d3d4953c0e8d07cd1244879628d084948e48ce6b6d808c5938f7858d" dependencies = [ "base64 0.23.1", "bytes", @@ -3606,7 +3603,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "foreign-types", "libc", @@ -4060,7 +4057,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", + "toml_edit 0.25.15+spec-1.1.0", ] [[package]] @@ -4252,7 +4249,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -4261,7 +4258,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -4281,7 +4278,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -4424,7 +4421,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustls 0.23.44", + "rustls 0.23.45", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4668,11 +4665,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -4693,9 +4690,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.44" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -4747,7 +4744,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.44", + "rustls 0.23.45", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.15", @@ -4889,7 +4886,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4959,7 +4956,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5055,7 +5052,7 @@ dependencies = [ "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5195,9 +5192,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" @@ -5324,9 +5321,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -5353,6 +5350,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "syslog" version = "7.0.0" @@ -5371,7 +5379,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5442,7 +5450,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5507,18 +5515,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -5545,7 +5544,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5564,7 +5563,7 @@ version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ - "rustls 0.23.44", + "rustls 0.23.45", "tokio", ] @@ -5674,9 +5673,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap 2.14.2", "toml_datetime 1.1.1+spec-1.1.0", @@ -5733,7 +5732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-core", "futures-util", @@ -5881,9 +5880,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-segmentation" @@ -5936,9 +5935,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -6019,7 +6018,7 @@ dependencies = [ "rocket", "rocket_ws", "rpassword", - "rustls 0.23.44", + "rustls 0.23.45", "semver", "serde", "serde_json", @@ -6134,7 +6133,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "wasm-bindgen-shared", ] @@ -6612,14 +6611,14 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -6666,14 +6665,14 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -6726,14 +6725,14 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "zlib-rs" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index d3a3d5e9..7dcae503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,11 +125,11 @@ libsqlite3-sys = { version = "0.38.2", optional = true } # Crypto-related libraries rand = "0.10.2" ring = "0.17.14" -rustls = { version = "0.23.44", features = ["ring", "std"], default-features = false } +rustls = { version = "0.23.45", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation -uuid = { version = "1.26.0", features = ["v4"] } +uuid = { version = "1.26.1", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } @@ -143,7 +143,7 @@ job_scheduler_ng = "2.5.0" data-encoding = "2.11.1" # JWT library -jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust_crypto", "use_pem"] } +jsonwebtoken = { version = "11.1.0", default-features = false, features = ["rust_crypto", "use_pem"] } # TOTP library totp-lite = "2.0.1" @@ -201,7 +201,7 @@ reqwest = { version = "0.13.5", default-features = false, features = [ "socks", "system-proxy", ] } -hickory-resolver = "0.26.2" +hickory-resolver = "0.26.3" # Favicon extraction libraries html5gum = "0.8.4" @@ -257,8 +257,8 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.59.1", default-features = false, features = ["services-fs"] } -opendal-http-transport-reqwest = { version = "0.59.1", default-features = false, features = ["rustls-no-provider"], optional = true } +opendal = { version = "0.59.2", default-features = false, features = ["services-fs"] } +opendal-http-transport-reqwest = { version = "0.59.2", default-features = false, features = ["rustls-no-provider"], optional = true } # For retrieving AWS credentials, including temporary SSO credentials aws-config = { version = "1.12.0", optional = true, default-features = false, features = [ @@ -268,7 +268,7 @@ aws-config = { version = "1.12.0", optional = true, default-features = false, fe "sso", ] } aws-credential-types = { version = "1.3.0", optional = true } -aws-smithy-runtime-api = { version = "1.16.0", optional = true } +aws-smithy-runtime-api = { version = "1.16.2", optional = true } http = { version = "1.5.0", optional = true } reqsign-aws-v4 = { version = "3.3.0", optional = true } reqsign-core = { version = "3.3.1", optional = true } diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 34cb913d..b397fbe1 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] quote = "1.0.47" -syn = "3.0.5" +syn = "3.0.6" [lints] workspace = true diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index 48b7400c..3a32d7ed 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-3.0.3 + * https://datatables.net/download/#bs5/dt-3.0.4 * * Included libraries: - * DataTables 3.0.3 + * DataTables 3.0.4 */ /*! DataTables Bootstrap 5 integration @@ -636,6 +636,18 @@ table.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order { left: 0.25rem; } +table.dataTable.table-sm > thead > tr th.dt-left .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-head-left .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-left .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-head-left .dt-column-order { + left: auto; + right: 0.25rem; +} +table.dataTable.table-sm > thead > tr th.dt-right .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-head-right .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-right .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-head-right .dt-column-order { + left: 0.25rem; + right: auto; +} div.dt-scroll-head table.table-bordered { border-bottom-width: 0; diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 1ae94cd2..3d4c727d 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,13 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-3.0.3 + * https://datatables.net/download/#bs5/dt-3.0.4 * * Included libraries: - * DataTables 3.0.3 + * DataTables 3.0.4 */ -/*! DataTables 3.0.3 +/*! DataTables 3.0.4 * Copyright (c) SpryMedia Ltd - datatables.net/license */ @@ -476,7 +476,7 @@ function assignDeep(out, ...inputs) { * Deep merge objects, but shallow copy arrays. The reason we need to do this, * is that we don't want to deep copy array init values (such as aaSorting) * since the dev wouldn't be able to override them, but we do want to deep copy - * arrays. + * objects. * * @param out Object to extend * @param extender Object from which the properties will be applied to out @@ -485,7 +485,6 @@ function assignDeep(out, ...inputs) { * present. This is so you can pass in a collection to DataTables and have * that used as your data source without breaking the references * @returns out Reference, just for convenience - out === the return. - * @todo This doesn't take account of arrays inside the deep copied objects. */ function assignDeepObjects(out, extender, breakRefs = false) { let val; @@ -496,7 +495,7 @@ function assignDeepObjects(out, extender, breakRefs = false) { if (!plainObject(out[prop])) { out[prop] = {}; } - assignDeep(out[prop], val); + assignDeepObjects(out[prop], val, breakRefs); } else if (breakRefs && prop !== 'data' && @@ -3241,5511 +3240,5542 @@ var pager = { numbers_length: 7 }; -const footer = (settings, cell, classes) => { - cell.classAdd(classes.tfoot.cell); -}; -const header = (settings, cell, classes) => { - cell.classAdd(classes.thead.cell); - if (!settings.features.ordering) { - cell.classAdd(classes.order.none); - } - var titleRow = settings.titleRow; - var headerRows = cell.closest('thead').find('tr'); - var rowIdx = cell.parent().index(); - // Conditions to not apply the ordering icons - if ( - // Cells and rows which have the attribute to disable the icons - cell.attr('data-dt-order') === 'disable' || - cell.parent().attr('data-dt-order') === 'disable' || - // titleRow support, for defining a specific row in the header - (titleRow === true && rowIdx !== 0) || - (titleRow === false && rowIdx !== headerRows.count() - 1) || - (typeof titleRow === 'number' && rowIdx !== titleRow)) { - return; - } - // No additional mark-up required. Attach a sort listener to update on sort - // - note that using the `DT` namespace will allow the event to be removed - // automatically on destroy, while the `dt` namespaced event is the one we - // are listening for - Dom.s(settings.table).on('order.dt.DT column-visibility.dt.DT', function (e, ctx, column) { - if (settings !== ctx) { - // need to check if this is the host - return; // table, not a nested one - } - var sorting = ctx.sortDetails; - if (!sorting) { - return; - } - var orderedColumns = pluck(sorting, 'col'); - // This handler is only needed on column visibility if the column is - // part of the ordering. If it isn't, then we can bail out to save - // performance. It could be a separate event handler, but this is a - // balance between code reuse / size and performance console.log(e, - // e.name, column, orderedColumns, orderedColumns.includes(column)) - if (e.type === 'column-visibility' && - !orderedColumns.includes(column)) { - return; - } - var i; - var orderClasses = classes.order; - var columns = ctx.api.columns(cell); - var col = settings.columns[columns.flatten()[0]]; - var orderable = columns.orderable().includes(true); - var ariaType = ''; - var indexes = columns.indexes(); - var sortDirs = columns.orderable(true).flatten(); - var tabIndex = settings.tabIndex; - var canOrder = ctx.orderHandler && orderable; - cell.classRemove(orderClasses.isAsc + ' ' + orderClasses.isDesc) - .classToggle(orderClasses.none, !orderable) - .classToggle(orderClasses.canAsc, canOrder && sortDirs.includes('asc')) - .classToggle(orderClasses.canDesc, canOrder && sortDirs.includes('desc')); - // Determine if all of the columns that this cell covers are - // included in the current ordering - var isOrdering = true; - for (i = 0; i < indexes.length; i++) { - if (!orderedColumns.includes(indexes[i])) { - isOrdering = false; - } - } - if (isOrdering) { - // Get the ordering direction for the columns under this cell - // Note that it is possible for a cell to be asc and desc - // sorting (column spanning cells) - var orderDirs = columns.order(); - cell.classAdd((orderDirs.includes('asc') ? orderClasses.isAsc : '') + - (orderDirs.includes('desc') ? orderClasses.isDesc : '')); - } - // Find the first visible column that has ordering applied to it - - // it get's the aria information, as the ARIA spec says that only - // one column should be marked with aria-sort - var firstVis = -1; // column index - for (i = 0; i < orderedColumns.length; i++) { - if (settings.columns[orderedColumns[i]].visible) { - firstVis = orderedColumns[i]; - break; - } - } - if (indexes[0] == firstVis) { - var firstSort = sorting[0]; - var sortOrder = col.orderSequence; - cell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending'); - // Determine if the next click will remove sorting or change the - // sort - ariaType = - sortOrder && !sortOrder[firstSort.index + 1] - ? 'Remove' - : 'Reverse'; - } - else { - cell.attrRemove('aria-sort'); - } - // Make the headers tab-able for keyboard navigation - if (orderable) { - var orderSpan = cell.find('.dt-column-order'); - orderSpan - .attr('role', 'button') - .attr('aria-label', orderable - ? col.ariaTitle + - ctx.api.i18n('aria.orderable' + ariaType) - : col.ariaTitle); - if (tabIndex !== -1) { - orderSpan.attr('tabindex', tabIndex); - } - } - }); +const defaults$4 = { + addedClasses: [], + cells: [], + data: [], + details: undefined, + detailsShow: undefined, + displayData: null, + idx: -1, + orderCache: null, + searchCellCache: null, + searchRowCache: null, + src: 'dom', + tr: null }; -const layout = (settings, container, items) => { - let classes = settings.classes.layout; - let row = Dom - .c('div') - .attr('id', items.id || null) - .classAdd(items.className || classes.row) - .appendTo(container); - displayRowCells(items, function (key, val) { - var klass = ''; - if (val.table) { - row.classAdd(classes.tableRow); - klass += classes.tableCell + ' '; - } - if (key === 'start') { - klass += classes.start; - } - else if (key === 'end') { - klass += classes.end; - } - else { - klass += classes.full; - } - Dom.c('div') - .attr({ - id: val.id || null, - class: val.className - ? val.className - : classes.cell + ' ' + klass - }) - .append(val.contents) - .appendTo(row); +/** + * Create a new object that is a row model + * + * @param parts Values to assign, otherwise the defaults will be used + * @returns New object + */ +function create$2(parts = {}) { + return util.object.assignDeep({}, defaults$4, parts); +} + +/** + * Add a data array to the table, creating DOM node etc. This is the parallel to + * gatherData, but for adding rows from a JavaScript source, rather than a + * DOM source. + * + * @param settings DataTables settings object + * @param dataIn data array to be added + * @param tr TR element to add to the table - optional. If not given, DataTables + * will create a row automatically + * @param tds Array of TD|TH elements for the row - must be given if tr is. + * @returns >=0 if successful (index of new data entry), -1 if failed + */ +function addData(settings, dataIn, tr, tds) { + /* Create the object for storing information about this new row */ + var rowIdx = settings.data.length; + var row = create$2({ + src: tr ? 'dom' : 'data', + idx: rowIdx }); -}; -const pagingButton = (settings, buttonType, content, active, disabled) => { - var classes = settings.classes.paging; - var btnClasses = [classes.button]; - var btn; - if (active) { - btnClasses.push(classes.active); - } - if (disabled) { - btnClasses.push(classes.disabled); + row.data = dataIn; + settings.data.push(row); + var columns = settings.columns; + for (var i = 0, iLen = columns.length; i < iLen; i++) { + // Invalidate the column types as the new data needs to be revalidated + columns[i].type = null; } - if (buttonType === 'ellipsis') { - btn = Dom.c('span').classAdd('ellipsis').html(content).get(0); + /* Add to the display array */ + settings.displayMaster.push(rowIdx); + var id = settings.rowIdFn(dataIn); + if (id !== undefined) { + settings.ids[id] = row; } - else { - btn = Dom - .c('button') - .classAdd(btnClasses.join(' ')) - .attr('role', 'link') - .attr('type', 'button') - .html(content) - .get(0); + /* Create the DOM information, or register it if already present */ + if (tr || !settings.features.deferRender) { + createTr(settings, rowIdx, tr, tds); } - return { - display: btn, - clicker: btn - }; -}; -const pagingContainer = (settings, buttons) => { - // No wrapping element - just append directly to the host - return buttons; -}; -function displayRowCells(items, fn) { - if (items.start) { - fn('start', items.start); + return rowIdx; +} +/** + * Add one or more TR elements to the table. Generally we'd expect to + * use this for reading data from a DOM sourced table, but it could be + * used for an TR element. Note that if a TR is given, it is used (i.e. + * it is not cloned). + * + * @param settings DataTables settings object + * @param rows The TR element(s) to add to the table + * @returns Array of indexes for the added rows + */ +function addTr(settings, rows) { + return rows.mapTo(el => { + let row = getRowElementsFromNode(settings, el); + return addData(settings, row.data, el, row.cells); + }); +} +/** + * Get the data for a given cell from the internal cache, taking into account + * data mapping + * + * @param settings DataTables settings object + * @param rowIdx data row id + * @param colIdx Column index + * @param type data get type ('display', 'type' 'filter|search' 'sort|order') + * @returns Cell data + */ +function getCellData(settings, rowIdx, colIdx, type) { + if (type === 'search') { + type = 'filter'; } - if (items.end) { - fn('end', items.end); + else if (type === 'order') { + type = 'sort'; } - if (items.full) { - fn('full', items.full); + var row = settings.data[rowIdx]; + if (!row) { + return undefined; } -} - -const store = { - className: {}, - detect: [], - render: {}, - search: {}, - order: {} -}; -// Common function to remove new lines, strip HTML and diacritic control -function _filterString(stripHtml, normalize) { - return function (str) { - if (util.is.empty(str) || typeof str !== 'string') { - return str; - } - str = str.replace(util.regex.reNewLines, ' '); - if (stripHtml) { - str = util.stripHtml(str); - } - { - str = util.diacritics(str, false); + var draw = settings.drawCount; + var col = settings.columns[colIdx]; + var rowData = row.data; + var defaultContent = col.defaultContent; + var cellData = col.dataGet(rowData, type, { + settings: settings, + row: rowIdx, + col: colIdx + }); + // Allow for a node being returned for non-display types + if (type !== 'display' && + cellData && + typeof cellData === 'object' && + cellData.nodeName) { + cellData = cellData.innerHTML; + } + if (cellData === undefined) { + if (settings.drawError != draw && defaultContent === null) { + log(settings, 0, 'Requested unknown parameter ' + + (typeof col.data == 'function' + ? '{function}' + : "'" + col.data + "'") + + ' for row ' + + rowIdx + + ', column ' + + colIdx, 4); + settings.drawError = draw; } - return str; - }; -} -function __numericReplace(d, decimalPlace, re1, re2) { - if (d !== 0 && (!d || d === '-')) { - return -Infinity; + return defaultContent; } - if (typeof d === 'number' || typeof d === 'bigint') { - return d; + // When the data source is null and a specific data type is requested (i.e. + // not the original data), we can use default column data + if ((cellData === rowData || cellData === null) && + defaultContent !== null && + type !== undefined) { + cellData = defaultContent; } - // If a decimal place other than `.` is used, it needs to be given to the - // function so we can detect it and replace with a `.` which is the only - // decimal place JavaScript recognises - it is not locale aware. - if (decimalPlace) { - d = util.conv.numToDecimal(d, decimalPlace); + else if (typeof cellData === 'function') { + // If the data source is a function, then we run it and use the return, + // executing in the scope of the data object (for instances) + return cellData.call(rowData); } - if (typeof d === 'string') { - if (re1) { - d = d.replace(re1, ''); - } - if (re2) { - d = d.replace(re2, ''); + if (cellData === null && type === 'display') { + return ''; + } + if (type === 'filter') { + var formatters = ext.type.search; + if (col.type && formatters[col.type]) { + cellData = formatters[col.type](cellData); } } - return d * 1; + return cellData; } -function register$1(name, prop, val) { - if (!prop) { - return { - className: store.className[name], - detect: store.detect.find(function (fn) { - return fn._name === name; - }), - order: { - pre: store.order[name + '-pre'], - asc: store.order[name + '-asc'], - desc: store.order[name + '-desc'] - }, - render: store.render[name], - search: store.search[name] - }; - } - var setProp = function (prop2, propVal) { - store[prop2][name] = propVal; - }; - var setDetect = function (detect) { - // `detect` can be a function or an object - we set a name - // property for either - that is used for the detection - Object.defineProperty(detect, '_name', { value: name }); - var idx = store.detect.findIndex(function (item) { - return item._name === name; +/** + * Set the value for a specific cell, into the internal data cache + * + * @param settings DataTables settings object + * @param rowIdx data row id + * @param colIdx Column index + * @param val Value to set + */ +function setCellData(settings, rowIdx, colIdx, val) { + let row = settings.data[rowIdx]; + if (row) { + let col = settings.columns[colIdx]; + let rowData = row.data; + col.dataSet(rowData, val, { + settings: settings, + row: rowIdx, + col: colIdx }); - if (idx === -1) { - store.detect.unshift(detect); - } - else { - store.detect.splice(idx, 1, detect); - } - }; - var setOrder = function (obj) { - store.order[name + '-pre'] = obj.pre; // can be undefined - store.order[name + '-asc'] = obj.asc; // can be undefined - store.order[name + '-desc'] = obj.desc; // can be undefined - }; - // prop is optional - if (val === undefined) { - val = prop; - prop = undefined; - } - if (prop === 'className') { - setProp('className', val); } - else if (prop === 'detect') { - setDetect(val); +} +/** + * Write a value to a cell + * + * @param td Cell + * @param val Value + */ +function writeCell(td, val) { + let cell = Dom.s(td); + if (val && typeof val === 'object' && val.nodeName) { + cell.empty().append(val); } - else if (prop === 'order') { - setOrder(val); + else { + cell.html(val); } - else if (prop === 'render') { - setProp('render', val); +} +/** + * Return an array with the full table data + * + * @param settings DataTables settings object + * @returns array {array} aData Master data array + */ +function getDataMaster(settings) { + return util.array.pluck(settings.data, 'data'); +} +/** + * Nuke the table + * + * @param settings DataTables settings object + */ +function clearTable(settings) { + settings.data.length = 0; + settings.displayMaster.length = 0; + settings.display.length = 0; + settings.ids = {}; +} +/** + * Mark cached data as invalid such that a re-read of the data will occur when + * the cached data is next requested. Also update from the data source object. + * + * @param settings DataTables settings object + * @param rowIdx Row index to invalidate + * @param src Source to invalidate from: undefined, 'auto', 'dom' or 'data' + * @param colIdx Column index to invalidate. If undefined the whole row will be + * invalidated + */ +function invalidateRow(settings, rowIdx, src, colIdx) { + var row = settings.data[rowIdx]; + var i, iLen; + if (!row) { + return; } - else if (prop === 'search') { - setProp('search', val); + // Remove the cached data for the row + row.orderCache = null; + row.searchCellCache = null; + row.displayData = null; + // Are we reading last data from DOM or the data object? + if (src === 'dom' || ((!src || src === 'auto') && row.src === 'dom')) { + // Read the data from the DOM + row.data = getRowElementsFromModel(settings, row, colIdx).data; } - else if (!prop) { - if (val.className) { - setProp('className', val.className); - } - if (val.detect !== undefined) { - setDetect(val.detect); - } - if (val.order) { - setOrder(val.order); - } - if (val.render !== undefined) { - setProp('render', val.render); - } - if (val.search !== undefined) { - setProp('search', val.search); + else { + // Reading from data object, update the DOM + var cells = row.cells; + var display = getRowDisplay(settings, rowIdx); + if (cells.length) { + if (colIdx !== undefined) { + writeCell(cells[colIdx], display[colIdx]); + } + else { + for (i = 0, iLen = cells.length; i < iLen; i++) { + writeCell(cells[i], display[i]); + } + } } } + invalidColumn(settings, colIdx); + // Update DataTables special `DT_*` attributes for the row + rowAttributes(settings, row); + callbackFire(settings, null, 'rowInvalidate', [settings, rowIdx, colIdx], false); } -// Get a list of types -function types() { - return store.detect.map(function (detect) { - return detect._name; - }); -} -var __diacriticSort = function (a, b) { - a = a !== null && a !== undefined ? a.toString().toLowerCase() : ''; - b = b !== null && b !== undefined ? b.toString().toLowerCase() : ''; - // Checked for `navigator.languages` support in `oneOf` so this code can't execute in old - // Safari and thus can disable this check - // eslint-disable-next-line compat/compat - return a.localeCompare(b, navigator.languages[0] || navigator.language, { - numeric: true, - ignorePunctuation: true - }); -}; -var __diacriticHtmlSort = function (a, b) { - a = util.stripHtml(a); - b = util.stripHtml(b); - return __diacriticSort(a, b); -}; -// -// Built in data types -// -register$1('string', { - detect: function () { - return 'string'; - }, - order: { - pre: function (a) { - // This is a little complex, but faster than always calling toString, - // http://jsperf.com/tostring-v-check - return util.is.empty(a) && typeof a !== 'boolean' - ? '' - : typeof a === 'string' - ? a.toLowerCase() - : !a.toString - ? '' - : a.toString(); - } - }, - search: _filterString(false) -}); -register$1('string-utf8', { - detect: { - allOf: function () { - return true; - }, - oneOf: function (d) { - // At least one data point must contain a non-ASCII character - // This line will also check if navigator.languages is supported or not. If not (Safari 10.0-) - // this data type won't be supported. - // eslint-disable-next-line compat/compat - return (!util.is.empty(d) && - navigator.languages && - typeof d === 'string' && - !!d.match(/[^\x00-\x7F]/)); - } - }, - order: { - asc: __diacriticSort, - desc: function (a, b) { - return __diacriticSort(a, b) * -1; - } - }, - search: _filterString(false) -}); -register$1('html', { - detect: { - allOf: function (d) { - return (util.is.empty(d) || - (typeof d === 'string' && d.indexOf('<') !== -1)); - }, - oneOf: function (d) { - // At least one data point must contain a `<` - return (!util.is.empty(d) && - typeof d === 'string' && - d.indexOf('<') !== -1); - } - }, - order: { - pre: function (a) { - return util.is.empty(a) - ? '' - : a.replace - ? util.stripHtml(a).trim().toLowerCase() - : a + ''; - } - }, - search: _filterString(true) -}); -register$1('html-utf8', { - detect: { - allOf: function (d) { - return (util.is.empty(d) || - (typeof d === 'string' && d.indexOf('<') !== -1)); - }, - oneOf: function (d) { - // At least one data point must contain a `<` and a non-ASCII character - // eslint-disable-next-line compat/compat - return (navigator.languages && - !util.is.empty(d) && - typeof d === 'string' && - d.indexOf('<') !== -1 && - typeof d === 'string' && - !!d.match(/[^\x00-\x7F]/)); - } - }, - order: { - asc: __diacriticHtmlSort, - desc: function (a, b) { - return __diacriticHtmlSort(a, b) * -1; - } - }, - search: _filterString(true) -}); -register$1('date', { - className: 'dt-type-date', - detect: { - allOf: function (d) { - // V8 tries _very_ hard to make a string passed into `Date.parse()` - // valid, so we need to use a regex to restrict date formats. Use a - // plug-in for anything other than ISO8601 style strings - if (d && !(d instanceof Date) && !util.regex.reDate.test(d)) { - return null; - } - var parsed = Date.parse(d); - return (parsed !== null && !isNaN(parsed)) || util.is.empty(d); - }, - oneOf: function (d) { - // At least one entry must be a date or a string with a date - return (d instanceof Date || - (typeof d === 'string' && util.regex.reDate.test(d))); - } - }, - order: { - pre: function (d) { - var ts = Date.parse(d); - return isNaN(ts) ? -Infinity : ts; - } +/** + * Column specific invalidation + * + * @param settings DataTables settings object + * @param colIdx Column index to invalidate, or all columns if not given + */ +function invalidColumn(settings, colIdx) { + // Column specific invalidation + var cols = settings.columns; + if (colIdx !== undefined) { + // Type - the data might have changed + cols[colIdx].type = null; + // Max length string. Its a fairly cheep recalculation, so not worth + // something more complicated + cols[colIdx].wideStrings = null; } -}); -register$1('html-num-fmt', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, true, false); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, true, false); - } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp, util.regex.reHtml, util.regex.reFormattedNumeric); - } - }, - search: _filterString(true) -}); -register$1('html-num', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, false, true); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, false, false); - } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp, util.regex.reHtml); - } - }, - search: _filterString(true) -}); -register$1('num-fmt', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.num(d, decimal, true, true); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.num(d, decimal, true, false); - } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp, util.regex.reFormattedNumeric); + else { + for (let i = 0, iLen = cols.length; i < iLen; i++) { + cols[i].type = null; + cols[i].wideStrings = null; } } -}); -register$1('num', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.num(d, decimal, false, true); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.num(d, decimal, false, false); + settings.containerWidth = -1; +} +/** + * Get the cells and data for a given row - from a element + * + * @param settings DataTables settings object + * @param row TR element from which to read data or existing row object from + * which to re-read the data from the cells + */ +function getRowElementsFromNode(settings, row) { + let data = settings.rowReadObject ? {} : []; + let cells = Dom.s(row).children('th, td'); + let id = row.getAttribute('id'); + cells.each((el, idx) => { + readCellData(settings, el, data, idx); + }); + if (id) { + util.set(settings.rowId)(data, id); + } + return { + data: data, + cells: cells.get() + }; +} +/** + * Get the cells and data for a given row - from an existing row model + * + * @param settings DataTables settings object + * @param row Existing row object from which to re-read the data from the cells + * @param colIdx Optional column index + */ +function getRowElementsFromModel(settings, row, colIdx) { + let tds = row.cells; + for (let i = 0; i < tds.length; i++) { + if (colIdx === undefined || colIdx === i) { + readCellData(settings, tds[i], row.data, i); } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp); + } + // Read the ID from the DOM if present + if (row.tr) { + let id = row.tr.getAttribute('id'); + if (id) { + util.set(settings.rowId)(row.data, id); } } -}); - + return { + data: row.data, + cells: tds + }; +} /** - * DataTables extensions - * - * This namespace acts as a collection area for plug-ins that can be used to - * extend DataTables capabilities. Indeed many of the build in methods - * use this method to provide their own capabilities (sorting methods for - * example). + * Read data from a cell into the data source object * - * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy - * reasons + * @param settings DataTables settings object + * @param cell The HTML cell element to read from + * @param data Data object / array to store data into + * @param colIdx The column index for the cell */ -const ext = { - /** - * DataTables build type (expanded by the download builder) - */ - builder: 'bs5/dt-3.0.3', - /** - * Buttons. For use with the Buttons extension for DataTables. This is - * defined here so other extensions can define buttons regardless of load - * order. It is _not_ used by DataTables core. - */ - buttons: {}, - /** - * ColumnControl buttons and content - */ - ccContent: {}, - /** - * Element class names - */ - classes: classes$1, - /** - * Error reporting. - * - * How should DataTables report an error. Can take the value 'alert', - * 'throw', 'none' or a function. - */ - errMode: 'alert', - /** HTML entity escaping */ - escape: { - /** When reading data-* attributes for initialisation options */ - attributes: false - }, - /** - * Legacy so v1 plug-ins don't throw js errors on load - */ - feature: legacy, - /** - * Feature plug-ins. - * - * This is an object of callbacks which provide the features for DataTables - * to be initialised via the `layout` option. - */ - features: features, - /** - * Row searching. - * - * This method of searching is complimentary to the default type based - * searching, and a lot more comprehensive as it allows you complete control - * over the searching logic. Each element in this array is a function - * (parameters described below) that is called for every row in the table, - * and your logic decides if it should be included in the searching data set - * or not. - */ - search: [], - /** - * Selector extensions - * - * The `selector` option can be used to extend the options available for the - * selector modifier options (`selector-modifier` object data type) that - * each of the three built in selector types offer (row, column and cell + - * their plural counterparts). For example the Select extension uses this - * mechanism to provide an option to select only rows, columns and cells - * that have been marked as selected by the end user (`{selected: true}`), - * which can be used in conjunction with the existing built in selector - * options. - */ - selector: { - cell: [], - column: [], - row: [] - }, - settings: [], - /** - * Legacy configuration options. Enable and disable legacy options that - * are available in DataTables. - * - * @type object - */ - legacy: { - /** - * Enable / disable DataTables 1.9 compatible server-side processing - * requests - */ - ajax: null - }, - /** - * Pagination plug-in methods. - * - * Each entry in this object is a function and defines which buttons should - * be shown by the pagination rendering method that is used for the table. - * The renderer addresses how the buttons are displayed in the document, - * while the functions here tell it what buttons to display. This is done by - * returning an array of button descriptions (what each button will do). - */ - pager: pager, - renderer: { - footer: { - _: footer - }, - header: { - _: header - }, - layout: { - _: layout - }, - pagingButton: { - _: pagingButton - }, - pagingContainer: { - _: pagingContainer +function readCellData(settings, cell, data, colIdx) { + let column = settings.columns[colIdx]; + let contents = cell.innerHTML.trim(); + if (column.attrSrc) { + // If we are working with attributes from the cell as values + let dataPoint = column.data; + let setter = util.set(dataPoint._); + let attr = function (str, cell) { + if (typeof str === 'string') { + let idx = str.indexOf('@'); + if (idx !== -1) { + let att = str.substring(idx + 1); + let setter = util.set(str); + setter(data, cell.getAttribute(att)); + } + } + }; + setter(data, contents); + attr(dataPoint.sort, cell); + attr(dataPoint.type, cell); + attr(dataPoint.filter, cell); + } + else { + if (!column.setter) { + // Cache the setter function + column.setter = util.set(column.data); } - }, - /** - * Rendering helper function exposed for use by the styling integrations. - */ - rendererDisplayRowCells: displayRowCells, - /** - * Ordering plug-ins - custom data source - * - * The extension options for ordering of data available here is - * complimentary to the default type based ordering that DataTables - * typically uses. It allows much greater control over the data that is - * being used to order a column, but is necessarily therefore more complex. - */ - order: {}, - /** - * Type based plug-ins. - * - * Each column in DataTables has a type assigned to it, either by automatic - * detection or by direct assignment using the `type` option for the column. - * The type of a column will effect how it is ordering and search (plug-ins - * can also make use of the column type if required). - */ - type: store, - /** - * Unique DataTables instance counter - * - * @type int - * @private - */ - _unique: 0, - // - // Depreciated - // The following properties are retained for backwards compatibility only. - // The should not be used in new projects and will be removed in a future - // version - // - /** - * Software version - * @type string - */ - version: '3.0.3' -}; -// -// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts -// -Object.assign(ext, { - afnFiltering: ext.search, - aTypes: ext.type.detect, - ofnSearch: ext.type.search, - oSort: ext.type.order, - afnSortData: ext.order, - aoFeatures: ext.feature, - oStdClasses: ext.classes, - oPagination: ext.pager, - sVersion: ext.version, - fnVersionCheck: check$1 -}); + column.setter(data, contents); + } +} /** - * Log an error message + * Generate the node required for the processing node + * + * @param ctx DataTables settings object + */ +function processingHtml(ctx) { + var table = ctx.table; + var scrolling = ctx.scroll.x !== '' || ctx.scroll.y !== ''; + if (ctx.features.processing) { + var n = Dom + .c('div') + .attr('id', ctx.tableId + '_processing') + .attr('role', 'status') + .classAdd(ctx.classes.processing.container) + .html(ctx.language.processing) + .append(Dom + .c('div') + .append(Dom.c('div')) + .append(Dom.c('div')) + .append(Dom.c('div')) + .append(Dom.c('div'))); + // Different positioning depending on if scrolling is enabled or not + if (scrolling) { + n.prependTo(Dom.s(ctx.tableWrapper).find('div.dt-scroll').get(0)); + } + else { + n.insertBefore(table); + } + Dom.s(table).on('processing.dt.DT', (e, s, show) => { + n.css('display', show ? 'block' : 'none'); + }); + } +} +/** + * Display or hide the processing indicator * * @param ctx DataTables settings object - * @param level log error messages, or display them to the user - * @param msg error message - * @param tn Technical note id to get more information about the error. + * @param show Show the processing indicator (true) or not (false) */ -function log(ctx, level, msg, tn) { - msg = - 'DataTables warning: ' + - (ctx ? 'table id=' + ctx.tableId + ' - ' : '') + - msg; - if (tn) { - msg += - '. For more information about this error, please see ' + - 'https://datatables.net/tn/' + - tn; +function processingDisplay(ctx, show) { + // Ignore cases when we are still redrawing + if (ctx.doingDraw && show === false) { + return; } - { - // Backwards compatibility pre 1.10 - var type = ext.sErrMode || ext.errMode; - if (ctx) { - callbackFire(ctx, null, 'dt-error', [ctx, tn, msg], true); - } - if (type == 'alert') { - alert(msg); - } - else if (type == 'throw') { - throw new Error(msg); - } - else if (typeof type == 'function') { - type(ctx, tn, msg); - } + callbackFire(ctx, null, 'processing', [ctx, show]); +} +/** + * Show the processing element if an action takes longer than a given time + * + * @param ctx DataTables settings object + * @param enable Do (true) or not (false) async processing (local feature enablement) + * @param run Function to run + */ +function processingRun(ctx, enable, run) { + if (!enable) { + // Immediate execution, synchronous + run(); + } + else { + processingDisplay(ctx, true); + // Allow the processing display to show if needed + setTimeout(function () { + run(); + processingDisplay(ctx, false); + }, 0); + } +} + +function renderer(ctx, type) { + var render = ctx.renderer; + var host = ext.renderer[type]; + if (plainObject(render) && render[type]) { + // Specific renderer for this type. If available use it, otherwise use + // the default. + return host[render[type]] || host._; + } + else if (typeof render === 'string') { + // Common renderer - if there is one available for this type use it, + // otherwise use the default + return host[render] || host._; + } + // Use the default + return host._; +} + +/** + * Recalculate the column widths, if needed (by a column having been + * invalidated) + * + * @param settings DataTables settings object + */ +function columnWidths(settings) { + if (settings.columns.map(c => c.wideStrings).includes(null)) { + calculateColumnWidths(settings); } } /** - * See if a property is defined on one object, if so assign it to the other - * object + * Calculate the width of columns for the table * - * @param ret target object - * @param src source object - * @param name property - * @param mappedName name to map too - optional, name used if not given + * @param settings DataTables settings object */ -function map(ret, src, name, mappedName) { - if (Array.isArray(name)) { - for (let i = 0; i < name.length; i++) { - let val = name[i]; - if (Array.isArray(val)) { - map(ret, src, val[0], val[1]); +function calculateColumnWidths(settings) { + // Not interested in doing column width calculation if auto-width is disabled + if (!settings.features.autoWidth) { + return; + } + var table = settings.table, columns = settings.columns, scroll = settings.scroll, scrollY = scroll.y, scrollX = scroll.x, visibleColumns = getColumns(settings, 'visible'), tableWidthAttr = table.getAttribute('width'), // from DOM element + tableContainer = table.parentElement, i, j, column, columnIdx; + var styleWidth = table.style.width; + var containerWidth = wrapperWidth(settings); + // Don't re-run for the same width as the last time + if (containerWidth === settings.containerWidth) { + return false; + } + settings.containerWidth = containerWidth; + // If there is no width applied as a CSS style or as an attribute, we assume that + // the width is intended to be 100%, which is usually is in CSS, but it is very + // difficult to correctly parse the rules to get the final result. + if (!styleWidth && !tableWidthAttr) { + table.style.width = '100%'; + styleWidth = '100%'; + } + if (styleWidth && styleWidth.indexOf('%') !== -1) { + tableWidthAttr = styleWidth; + } + // Let plug-ins know that we are doing a recalc, in case they have changed any of the + // visible columns their own way (e.g. Responsive uses display:none). + callbackFire(settings, null, 'column-calc', [{ visible: visibleColumns }], false); + // Construct a worst case table with the widest, assign any user defined + // widths, then insert it into the DOM and allow the browser to do all + // the hard work of calculating table widths + var tmpTable = Dom + .s(table.cloneNode()) + .css('visibility', 'hidden') + .css('margin', '0') + .attrRemove('id'); + // Clean up the table body + tmpTable.append(Dom.c('tbody')); + // Clone the table header and footer - we can't use the header / footer + // from the cloned table, since if scrolling is active, the table's + // real header and footer are contained in different table tags + tmpTable + .append(settings.thead.cloneNode(true)) + .append(settings.tfoot.cloneNode(true)); + // Remove any assigned widths from the footer (from scrolling) + tmpTable.find('tfoot th, tfoot td').css('width', ''); + // Apply custom sizing to the cloned header + tmpTable.find('thead th, thead td').each(cell => { + // Get the `width` from the header layout + var width = columnsSumWidth(settings, cell, true); + if (width) { + cell.style.width = width; + // For scrollX we need to force the column width otherwise the + // browser will collapse it. If this width is smaller than the + // width the column requires, then it will have no effect + if (scrollX) { + cell.style.minWidth = width; + Dom.s(cell).append(Dom.c('div').css({ + width: width, + margin: '0', + padding: '0', + border: '0', + height: '1px' + })); } - else { - map(ret, src, val); + } + else { + cell.style.width = ''; + } + }); + // Get the widest strings for each of the visible columns and add them to + // our table to create a "worst case" + var longestData = []; + for (i = 0; i < visibleColumns.length; i++) { + longestData.push(getWideStrings(settings, visibleColumns[i])); + } + if (longestData.length) { + for (i = 0; i < longestData[0].length; i++) { + var tr = Dom.c('tr').appendTo(tmpTable.find('tbody')); + for (j = 0; j < visibleColumns.length; j++) { + columnIdx = visibleColumns[j]; + column = columns[columnIdx]; + var longest = longestData[j][i] || ''; + var autoClass = ext.type.className[column.type]; + var padding = column.contentPadding || (scrollX ? '-' : ''); + var text = longest + padding; + var cell = Dom + .c('td') + .classAdd(autoClass) + .classAdd(column.className) + .appendTo(tr); + if (longest.indexOf('<') === -1 && + longest.indexOf('&') === -1) { + cell.text(text); + } + else { + cell.html(text); + } } } - return; } - if (mappedName === undefined) { - mappedName = name; + // Tidy the temporary table - remove name attributes so there aren't + // duplicated in the dom (radio elements for example) + tmpTable.find('[name]').attrRemove('name'); + // Table has been built, attach to the document so we can work with it. + // A holding element is used, positioned at the top of the container + // with minimal height, so it has no effect on if the container scrolls + // or not. Otherwise it might trigger scrolling when it actually isn't + // needed + var holder = Dom + .c('div') + .css(scrollX || scrollY + ? { + position: 'absolute', + top: '0', + left: '0', + height: '1px', + right: '0', + overflow: 'hidden' + } + : {}) + .append(tmpTable) + .appendTo(tableContainer); + // When scrolling (X or Y) we want to set the width of the table as + // appropriate. However, when not scrolling leave the table width as it + // is. This results in slightly different, but I think correct behaviour + if (scrollX) { + tmpTable.css('width', 'auto').attrRemove('width'); + // If there is no width attribute or style, then allow the table to + // collapse + if (tmpTable.width() < tableContainer.clientWidth && tableWidthAttr) { + tmpTable.width(tableContainer.clientWidth); + } } - if (src[name] !== undefined) { - ret[mappedName] = src[name]; + else if (scrollY) { + tmpTable.width(tableContainer.clientWidth); } -} -/** - * Bind an event handler to allow a click or return key to activate the callback. - * This is good for accessibility since a return on the keyboard will have the - * same effect as a click, if the element has focus. - * - * @param n Element to bind the action to - * @param selector Selector (for delegated events) - * @param fn Callback function for when the event is triggered - */ -function bindAction(n, selector, fn) { - Dom.s(n) - .on('click.DT', selector, function (e) { - fn(e); - }) - .on('keypress.DT', selector, function (e) { - if (e.which === 13) { - e.preventDefault(); - fn(e); - } - }) - .on('selectstart.DT', selector, function () { - // Don't want a double click resulting in text selection - return false; - }); -} -/** - * Register a callback function. Easily allows a callback function to be added - * to an array store of callback functions that can then all be called together. - * - * @param settings dataTables settings object - * @param store Name of the array storage for the callbacks in settings - * @param fn Function to be called back - */ -function callbackReg(ctx, store, fn) { - if (fn) { - ctx.callbacks[store].push(fn); + else if (tableWidthAttr) { + tmpTable.width(tableWidthAttr); } -} -/** - * Fire callback functions and trigger events. Note that the loop over the - * callback array store is done backwards! Further note that you do not want to - * fire off triggers in time sensitive applications (for example cell creation) - * as its slow. - * - * @param ctx DataTables settings object - * @param callbackArr Name of the array storage for the callbacks in the context - * @param eventName Name of the custom event to trigger. If null no trigger is - * fired - * @param args Array of arguments to pass to the callback function / trigger - * @param bubbles True if the event should bubble - */ -function callbackFire(ctx, callbackArr, eventName, args, bubbles = false) { - var ret = []; - if (callbackArr) { - ret = ctx.callbacks[callbackArr] - .slice() - .reverse() - .map(function (val) { - return val.apply(ctx.instance, args); - }); + // Get the width of each column in the constructed table + var total = 0; + var bodyCells = tmpTable.find('tbody tr').eq(0).children(); + for (i = 0; i < visibleColumns.length; i++) { + // Use getBounding for sub-pixel accuracy, which we then want to round + // up! + var bounding = bodyCells.get(i).getBoundingClientRect().width; + // Total is tracked to remove any sub-pixel errors as the outerWidth + // of the table might not equal the total given here + total += bounding; + // Width for each column to use + columns[visibleColumns[i]].width = stringToCss(bounding); } - if (eventName !== null) { - let table = Dom.s(ctx.table); - let result = table.trigger(eventName + '.dt', bubbles, args, { - dt: ctx.api + table.style.width = stringToCss(total); + // Finished with the table - ditch it + holder.remove(); + // If there is a width attr, we want to attach an event listener which + // allows the table sizing to automatically adjust when the window is + // resized. Use the width attr rather than CSS, since we can't know if the + // CSS is a relative value or absolute - DOM read is always px. + if (tableWidthAttr) { + table.style.width = stringToCss(tableWidthAttr); + } + if ((tableWidthAttr || scrollX) && !settings.reszEvt) { + var resize = util.throttle(function () { + var newWidth = wrapperWidth(settings); + // Don't do it if destroying or the container width is 0 + if (!settings.destroying && newWidth !== 0) { + adjustColumnSizing(settings); + } }); - // If not yet attached to the document, trigger the event - // on the body directly to sort of simulate the bubble - if (bubbles && table.closest('body').count() === 0) { - Dom.s('body').trigger(eventName + '.dt', bubbles, args, { - dt: ctx.api + // For browsers that support it (~2020 onwards for wide support) we can watch for the + // container changing width. + if (window.ResizeObserver) { + // This is a tricky beast - if the element is visible when `.observe()` is called, + // then the callback is immediately run. Which we don't want. If the element isn't + // visible, then it isn't run, but we want it to run when it is then made visible. + // This flag allows the above to be satisfied. + var first = Dom.s(settings.tableWrapper).isVisible(); + // Use an empty div to attach the observer so it isn't impacted by height changes + var resizer = Dom + .c('div') + .css({ + width: '100%', + height: '0' + }) + .classAdd('dt-autosize') + .appendTo(settings.tableWrapper); + settings.resizeObserver = new ResizeObserver(function (e) { + if (first) { + first = false; + } + else { + resize(); + } }); + settings.resizeObserver.observe(resizer.get(0)); } - ret.push(result[0]); - } - return ret; -} -function lengthOverflow(ctx) { - var start = ctx.displayStart, end = displayEnd(ctx), len = ctx.pageLength; - // If we have space to show extra rows (backing up from the end point - then - // do so - if (start >= end) { - start = end - len; - } - // Keep the start record on the current page - start -= start % len; - if (len === -1 || start < 0) { - start = 0; + else { + // For old browsers, the best we can do is listen for a window + // resize + window.addEventListener('resize', resize); + settings.windowResizeCb = resize; // For removal in `destroy` + } + settings.reszEvt = true; } - ctx.displayStart = start; } /** - * Detect the data source being used for the table. Used to simplify the code a - * little (ajax) and to make it compress a little smaller. + * Get the width of the DataTables wrapper element * - * @param ctx DataTables settings object - * @returns Data source + * @param settings DataTables settings object + * @returns Width */ -function dataSource(ctx) { - if (ctx.features.serverSide) { - return 'ssp'; - } - else if (ctx.ajax) { - return 'ajax'; - } - return 'dom'; +function wrapperWidth(settings) { + let wrapper = Dom.s(settings.tableWrapper); + return wrapper.isVisible() ? wrapper.width() : 0; } /** - * Common replacement for language strings + * Get the widest strings for each column. * - * @param ctx DataTables settings object - * @param str String with values to replace - * @param entries Plural number for _ENTRIES_ - can be undefined - * @returns String - */ -function macros(ctx, str, entries) { - // When infinite scrolling, we are always starting at 1. _iDisplayStart is - // used only internally - var formatter = ctx.formatNumber, start = ctx.displayStart + 1, len = ctx.pageLength, vis = recordsDisplay(ctx), max = recordsTotal(ctx), all = len === -1; - return str - .replace(/_START_/g, formatter(start, ctx)) - .replace(/_END_/g, formatter(displayEnd(ctx), ctx)) - .replace(/_MAX_/g, formatter(max, ctx)) - .replace(/_TOTAL_/g, formatter(vis, ctx)) - .replace(/_PAGE_/g, formatter(all ? 1 : Math.ceil(start / len), ctx)) - .replace(/_PAGES_/g, formatter(all ? 1 : Math.ceil(vis / len), ctx)) - .replace(/_ENTRIES_/g, ctx.api.i18n('entries', '', entries)) - .replace(/_ENTRIES-MAX_/g, ctx.api.i18n('entries', '', max)) - .replace(/_ENTRIES-TOTAL_/g, ctx.api.i18n('entries', '', vis)); -} -/** - * Add elements to an array as quickly as possible, but stack safe. + * It is very difficult to determine what the widest string actually is due to variable character + * width and kerning. Doing an exact calculation with the DOM or even Canvas would kill performance + * and this is a critical point, so we use two techniques to determine a collection of the longest + * strings from the column, which will likely contain the widest strings: * - * @param arr Array to add the data to - * @param data Data array that is to be added + * 1) Get the top three longest strings from the column + * 2) Get the top three widest words (i.e. an unbreakable phrase) + * + * @param settings DataTables settings object + * @param colIdx column of interest + * @returns Array of the longest strings */ -function arrayApply(arr, data) { - if (!data) { - return; - } - // Chrome can throw a max stack error if apply is called with - // too large an array, but apply is faster. - if (data.length < 10000) { - arr.push.apply(arr, data); - } - else { - for (var i = 0; i < data.length; i++) { - arr.push(data[i]); +function getWideStrings(settings, colIdx) { + var column = settings.columns[colIdx]; + // Do we need to recalculate (i.e. was invalidated), or just use the cached data? + if (!column.wideStrings) { + var allStrings = []; + var collection = []; + // Create an array with the string information for the column + for (var i = 0, iLen = settings.displayMaster.length; i < iLen; i++) { + var rowIdx = settings.displayMaster[i]; + var data = getRowDisplay(settings, rowIdx)[colIdx]; + var cellString = data && typeof data === 'object' && data.nodeType + ? data.innerHTML + : data + ''; + // Remove id / name attributes from elements so they + // don't interfere with existing elements + cellString = cellString + .replace(/id=".*?"/g, '') + .replace(/name=".*?"/g, ''); + // Don't want script, dialog or template tags in the width + // calculations as they are hidden content + cellString = cellString + .replace(/]*)?>/gi, ' ') + .replace(/]*)?>/gi, ' ') + .replace(/]*)?>/gi, ' '); + var noHtml = util.string + .stripHtml(cellString, ' ') + .replace(/ /g, ' '); + collection.push({ + str: cellString, + len: noHtml.length + }); + allStrings.push(noHtml); + } + // Order and then cut down to the size we need + collection + .sort(function (a, b) { + return b.len - a.len; + }) + .splice(3); + column.wideStrings = collection.map(function (item) { + return item.str; + }); + // Longest unbroken string + const parts = allStrings.join(' ').split(' '); + parts.sort(function (a, b) { + return b.length - a.length; + }); + if (parts.length) { + column.wideStrings.push(parts[0]); + } + if (parts.length > 1) { + column.wideStrings.push(parts[1]); + } + if (parts.length > 2) { + column.wideStrings.push(parts[3]); } } + return column.wideStrings; } /** - * Add one or more listeners to the table + * Append a CSS unit (only if required) to a string * - * @param that JQ for the table - * @param name Event name - * @param src Listener(s) + * @param s Value to css-ify + * @returns Value with css unit */ -function listener(that, name, src) { - let srcArr = Array.isArray(src) ? src : [src]; - for (var i = 0; i < srcArr.length; i++) { - that.on(name + '.dt.DT', srcArr[i]); +function stringToCss(s) { + if (s === null) { + return '0px'; } + if (typeof s == 'number') { + return s < 0 ? '0px' : s + 'px'; + } + // Check it has a unit character already + return s.match(/\d$/) ? s + 'px' : s; } /** - * Escape HTML entities in strings, in an object + * Re-insert the `col` elements for current visibility + * + * @param settings DT settings */ -function escapeObject(obj) { - if (ext.escape.attributes) { - each(obj, function (key, val) { - obj[key] = escapeHtml(val); - }); +function colGroup(settings) { + var cols = settings.columns; + settings.colgroup.empty(); + for (var i = 0; i < cols.length; i++) { + if (cols[i].visible) { + settings.colgroup.append(cols[i].colEl); + } } - return obj; } -/* - * Public helper functions. These aren't used internally by DataTables, or - * called by any of the options passed into DataTables, but they can be used - * externally by developers working with DataTables. They are helper functions - * to make working with DataTables a little bit easier. - */ /** - * Common logic for moment, luxon or a date action. + * Scrolling setup * - * Happens after __mldObj, so don't need to call `resolveWindowsLibs` again + * @param settings DataTables settings object + * @returns Node to add to the DOM */ -function __mld(dtLib, momentFn, luxonFn, dateFn, arg1) { - if (__moment) { - return dtLib[momentFn](arg1); +function featureTable(settings) { + let table = Dom.s(settings.table); + let scroll = settings.scroll; + let scrollX = scroll.x; + let scrollY = scroll.y; + // No scrolling or x-scrolling only + if (scrollY === '' && scrollX === '') { + return table.get(0); } - else if (__luxon) { - return dtLib[luxonFn](arg1); + let classes = settings.classes.scrolling; + let caption = settings.captionNode; + let captionSide = caption + ? caption._captionSide + : null; + let tableCloneHeader = table.clone(false); + let tableCloneFooter = table.clone(false); + let footer = table.children('tfoot'); + let size = function (s) { + return !s ? '100%' : stringToCss(s); + }; + /* + * The HTML structure that we want to generate in this function is: + * div - scroller + * div - scroll head + * div - scroll head inner + * table - scroll head table + * thead - thead + * div - scroll body + * table - table (master table) + * thead - thead clone for sizing + * tbody - tbody + * div - scroll foot + * div - scroll foot inner + * table - scroll foot table + * tfoot - tfoot + */ + let scroller = Dom.c('div') + .classAdd(classes.container) + .attr('role', 'table') + .append(Dom.c('div') + .classAdd(classes.header.self) + .css({ + overflow: 'hidden', + position: 'relative', + border: '0', + width: scrollX ? size(scrollX) : '100%' + }) + .attr('role', 'none') + .append(Dom.c('div') + .classAdd(classes.header.inner) + .css({ + 'box-sizing': 'content-box', + width: scroll.xInner || '100%' + }) + .attr('role', 'none') + .append(tableCloneHeader + .attrRemove('id') + .css('margin-left', '0') + .append(captionSide === 'top' ? caption : null) + .append(table.children('thead'))))) + .append(Dom.c('div') + .classAdd(classes.body) + .css({ + position: 'relative', + overflow: 'auto', + width: size(scrollX) + }) + .attr('role', 'none') + .append(table)); + if (footer.count()) { + scroller.append(Dom.c('div') + .classAdd(classes.footer.self) + .css({ + overflow: 'hidden', + border: '0', + width: scrollX ? size(scrollX) : '100%' + }) + .attr('role', 'none') + .append(Dom.c('div') + .classAdd(classes.footer.inner) + .attr('role', 'none') + .append(tableCloneFooter + .attrRemove('id') + .css('margin-left', '0') + .append(captionSide === 'bottom' ? caption : null) + .append(table.children('tfoot'))))); } - return dateFn ? dtLib[dateFn](arg1) : dtLib; + let children = scroller.children(); + let scrollHead = children.eq(0); + let scrollBody = children.eq(1); + let scrollFoot = children.eq(2); + // When the body is scrolled, then we also want to scroll the header and + // footer. Note that each element has its own scroll listener, and that in + // turn sets the scroll for the other elements. However this doesn't lead to + // an infinite loop as `scroll` is only triggered if the value changes. + scrollBody.on('scroll.DT', () => { + let scrollLeft = scrollBody.scrollLeft(); + scrollHead.scrollLeft(scrollLeft); + scrollFoot.scrollLeft(scrollLeft); + }); + scrollHead.on('scroll.DT', () => { + let scrollLeft = scrollHead.scrollLeft(); + scrollBody.scrollLeft(scrollLeft); + scrollFoot.scrollLeft(scrollLeft); + }); + scrollFoot.on('scroll.DT', () => { + let scrollLeft = scrollFoot.scrollLeft(); + scrollHead.scrollLeft(scrollLeft); + scrollBody.scrollLeft(scrollLeft); + }); + scrollBody.css('max-height', size(scrollY)); + if (!scroll.collapse) { + scrollBody.css('height', size(scrollY)); + } + settings.scrollHead = scrollHead; + settings.scrollBody = scrollBody; + settings.scrollFoot = scrollFoot; + // On redraw - align columns + settings.callbacks.draw.push(scrollDraw); + // Aria roles - because we break the table up into parts we need to be very + // explicit with the roles to create the accessability tree for the table, + // otherwise browser's attempt to "fix" the tree by filling in what it + // thinks are gaps. The static elements that we can assign roles to are done + // here. Dynamic ones are done in the draw function below. + table.attr('role', 'none'); + table.find('tbody').attr('role', 'rowgroup'); + tableCloneHeader.attr('role', 'none'); + tableCloneFooter.attr('role', 'none'); + settings.colgroup.find('colgroup').attr('role', 'none'); + // Move the info feature's aria desc by to the new "table" + let describedBy = table.attr('aria-describedby'); + if (describedBy) { + scroller.attr('aria-describedby', describedBy); + table.attrRemove('aria-describedby'); + } + return scroller.get(0); } -var __mlWarning = false; -var __luxon; -var __moment; /** + * Update the header, footer and body tables for resizing - i.e. column + * alignment. + * + * Welcome to the most horrible function DataTables. The process that this + * function follows is basically: + * 1. Re-create the table inside the scrolling div + * 2. Correct colgroup > col values if needed + * 3. Copy colgroup > col over to header and footer + * 4. Clean up * + * @param settings DataTables settings object */ -function resolveWindowLibs() { - __luxon = util.external('luxon'); - __moment = util.external('moment'); -} -function __mldObj(d, format, locale) { - var dt; - resolveWindowLibs(); - if (__moment) { - dt = __moment(d, format, locale, true); - if (!dt.isValid()) { - return null; - } - } - else if (__luxon) { - dt = - format && typeof d === 'string' - ? __luxon.DateTime.fromFormat(d, format) - : __luxon.DateTime.fromISO(d); - if (!dt.isValid) { - return null; - } - dt = dt.setLocale(locale); - } - else if (!format) { - // No format given, must be ISO - dt = new Date(d); +function scrollDraw(settings) { + // Given that this is such a monster function, a lot of variables are use + // to try and keep the minimised size as small as possible + let scroll = settings.scroll, barWidth = scroll.barWidth, divHeader = settings.scrollHead, divHeaderInner = divHeader.children('div'), divHeaderTable = divHeaderInner.children('table'), divBodyEl = settings.scrollBody, divBody = divBodyEl, divFooter = settings.scrollFoot, divFooterInner = divFooter.children('div'), divFooterTable = divFooterInner.children('table'), header = Dom.s(settings.thead), table = Dom.s(settings.table), footer = Dom.s(settings.tfoot), browser = settings.browser, headerCopy, footerCopy; + // If the scrollbar visibility has changed from the last draw, we need to + // adjust the column sizes as the table width will have changed to account + // for the scrollbar + let scrollBarVis = divBodyEl.get(0).scrollHeight > divBodyEl.get(0).clientHeight; + if (settings.scrollBarVis !== scrollBarVis && + settings.scrollBarVis !== undefined) { + settings.scrollBarVis = scrollBarVis; + adjustColumnSizing(settings); + return; // adjust column sizing will call this function again } else { - if (!__mlWarning) { - alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17'); - } - __mlWarning = true; + settings.scrollBarVis = scrollBarVis; } - return dt; -} -// Wrapper for date, datetime and time which all operate the same way with the -// exception of the output string for auto locale support -function __mlHelper(localeString) { - return function (from, to, locale, def) { - // Luxon and Moment support - // Argument shifting - if (arguments.length === 0) { - locale = 'en'; - to = null; // means toLocaleString - from = null; // means iso8601 - } - else if (arguments.length === 1) { - locale = 'en'; - to = from; - from = null; - } - else if (arguments.length === 2) { - locale = to; - to = from; - from = null; - } - var typeName = 'datetime' + (to ? '-' + to : ''); - // Add type detection and sorting specific to this date format - we need - // to be able to identify date type columns as such, rather than as - // numbers in extensions. Hence the need for this. - if (!store.order[typeName + '-pre']) { - register$1(typeName, { - detect: function (d) { - // The renderer will give the value to type detect as the - // type! - return d === typeName ? typeName : false; - }, - order: { - pre: function (d) { - // The renderer gives us Moment, Luxon or Date objects - // for the sorting, all of which have a `valueOf` which - // gives milliseconds epoch - return d.valueOf(); - } + header.find('thead').attr('role', 'rowgroup'); + footer.find('tfoot').attr('role', 'rowgroup'); + // 1. Re-create the table inside the scrolling div + // Remove the old minimised thead and tfoot elements in the inner table + table.children('thead, tfoot').remove(); + // Clone the current header and footer elements and then place it into the + // inner table + headerCopy = header.clone(true).prependTo(table); + headerCopy.find('th, td').attrRemove('tabindex'); + headerCopy.find('[id]').attrRemove('id'); + if (footer.count()) { + footerCopy = footer.clone(true).prependTo(table); + footerCopy.find('[id]').attrRemove('id'); + } + // 2. Correct colgroup > col values if needed + // It is possible that the cell sizes are smaller than the content, so we need to + // correct colgroup>col for such cases. This can happen if the auto width detection + // uses a cell which has a longer string, but isn't the widest! For example + // "Chief Executive Officer (CEO)" is the longest string in the demo, but + // "Systems Administrator" is actually the widest string since it doesn't collapse. + // Note the use of translating into a column index to get the `col` element. This + // is because of Responsive which might remove `col` elements, knocking the alignment + // of the indexes out. + if (settings.display.length) { + // Get the column sizes from the first row in the table. This should really be a + // [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year + // browser support + let firstTr = null; + let start = dataSource(settings) !== 'ssp' ? settings.displayStart : 0; + for (let i = start; i < start + settings.display.length; i++) { + let idx = settings.display[i]; + let row = settings.data[idx]; + if (row) { + let tr = row.tr; + if (tr) { + firstTr = tr; + break; } - }); - } - if (!store.className[typeName]) { - store.className[typeName] = 'dt-right'; + } } - return function (d, type) { - // Allow for a default value - if (d === null || d === undefined) { - if (def === '--now') { - // We treat everything as UTC further down, so no changes - // are made, as such need to get the local date / time as if - // it were UTC - var local = new Date(); - d = new Date(Date.UTC(local.getFullYear(), local.getMonth(), local.getDate(), local.getHours(), local.getMinutes(), local.getSeconds())); - } - else { - d = ''; + if (firstTr) { + let colSizes = Dom.s(firstTr) + .children('th, td') + .mapTo(function (cell, idx) { + return { + idx: visibleToColumnIndex(settings, idx), + width: Dom.s(cell).width('outer') + }; + }); + // Check against what the colgroup > col is set to and correct if needed + for (let i = 0; i < colSizes.length; i++) { + let colEl = settings.columns[colSizes[i].idx].colEl; + colEl.css('width', colSizes[i].width + 'px'); + if (scroll.x) { + colEl.css('minWidth', colSizes[i].width + 'px'); } } - if (type === 'type') { - // Typing uses the type name for fast matching - return typeName; - } - if (d === '') { - return type !== 'sort' - ? '' - : __mldObj('0000-01-01 00:00:00', null, locale); - } - // Shortcut. If `from` and `to` are the same, we are using the - // renderer to format for ordering, not display - its already in the - // display format. - if (to !== null && - from === to && - type !== 'sort' && - type !== 'type' && - !(d instanceof Date)) { - return d; - } - // Determine if there is a timezone. If there is, we want to reuse - // it for the output, so the timezone doesn't change between the - // input and output. - let options = {}; - let tzMatch = typeof d === 'string' ? d.match(util.regex.isoTimezone) : null; - if (tzMatch) { - options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1]; - } - // Get a Date object (Luxon, moment or Date) - var dt = __mldObj(d, from, locale); - if (dt === null) { - return d; - } - if (type === 'sort') { - return dt; - } - var formatted = to === null - ? __mld(dt, 'toDate', 'toJSDate', '')[localeString](navigator.language, options) - : __mld(dt, 'format', 'toFormat', 'toISOString', to); - // XSS protection - return type === 'display' ? util.escapeHtml(formatted) : formatted; - }; - }; -} -// Based on locale, determine standard number formatting -// Fallback for legacy browsers is US English -var __thousands = ','; -var __decimal = '.'; -if (window.Intl !== undefined) { - try { - var num = new Intl.NumberFormat().formatToParts(100000.1); - for (var i = 0; i < num.length; i++) { - if (num[i].type === 'group') { - __thousands = num[i].value; - } - else if (num[i].type === 'decimal') { - __decimal = num[i].value; - } } } - catch (e) { - // noop - } -} -/** - * Register a date / time format for DataTables to use. - * - * @param format The date / time format to detect data in. Please refer to the - * Moment.js or Luxon document for the full list of tokens, depending on which - * of the two libraries you are using. - * @param locale The locale to pass to Moment.js / Luxon. - */ -function datetime(format, locale) { - var typeName = 'datetime-' + format; - if (!locale) { - locale = 'en'; + // 3. Copy the colgroup over to the header and footer + divHeaderTable.find('colgroup').remove(); + divHeaderTable.append(settings.colgroup.clone(true)); + if (footer) { + divFooterTable.find('colgroup').remove(); + divFooterTable.append(settings.colgroup.clone(true)); } - if (!store.order[typeName]) { - register$1(typeName, { - detect: function (d) { - var dt = __mldObj(d, format, locale); - return d === '' || dt ? typeName : false; - }, - order: { - pre: function (d) { - return __mldObj(d, format, locale) || 0; - } - } + // "Hide" the header and footer that we used for the sizing. We need to keep + // the content of the cell so that the width applied to the header and body + // both match, but we want to hide it completely. + headerCopy.find('th, td').each(function (el) { + Dom.c('div') + .classAdd('dt-scroll-sizing') + .append(Array.from(el.childNodes)) + .appendTo(el); + }); + if (footerCopy) { + footerCopy.find('th, td').each(function (el) { + Dom.c('div') + .classAdd('dt-scroll-sizing') + .append(Array.from(el.childNodes)) + .appendTo(el); }); } - if (!store.className[typeName]) { - store.className[typeName] = 'dt-right'; + // 4. Clean up + // Figure out if there are scrollbar present - if so then we need the header and footer to + // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) + let isScrolling = Math.floor(table.height()) > divBodyEl.get(0).clientHeight || + divBody.css('overflow-y') == 'scroll'; + let paddingSide = 'padding' + (browser.scrollbarLeft ? 'Left' : 'Right'); + // Set the width's of the header and footer tables + let outerWidth = table.width('withPadding'); + divHeaderTable.css('width', stringToCss(outerWidth)); + divHeaderInner + .css('width', stringToCss(outerWidth)) + .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); + if (footer.count()) { + divFooterTable.css('width', stringToCss(outerWidth)); + divFooterInner + .css('width', stringToCss(outerWidth)) + .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); + } + // Correct DOM ordering for colgroup - comes before the thead + table.children('colgroup').prependTo(table); + // Remove tabindex from the hidden row elements + table.find('thead, tfoot').find('[tabindex]').attrRemove('tabindex'); + // Dynamic ARIA roles - see setup for details on why this is needed + table + .find('thead, tfoot') + .attr('role', 'none') + .find('[role]') + .attrRemove('role'); + table.find('tbody tr:not([role])').attr('role', 'row'); + table.find('tbody td:not([role]), tbody th:not([role])').attr('role', 'cell'); + scrollAria(headerCopy); + scrollAria(footerCopy); + // Adjust the position of the header in case we loose the y-scrollbar + divBody.trigger('scroll'); + // If sorting or filtering has occurred, jump the scrolling back to the top + // only if we aren't holding the position + if ((settings.wasOrdered || settings.wasFiltered) && !settings.drawHold) { + divBodyEl.scrollTop(0); } } /** - * Helpers for `columns.render`. - */ -var helpers = { - date: __mlHelper('toLocaleDateString'), - datetime: __mlHelper('toLocaleString'), - time: __mlHelper('toLocaleTimeString'), - number: function (thousands, decimal, precision, prefix, postfix) { - // Auto locale detection - if (thousands === null || thousands === undefined) { - thousands = __thousands; - } - if (decimal === null || decimal === undefined) { - decimal = __decimal; - } - return { - display: function (d) { - if (typeof d !== 'number' && typeof d !== 'string') { - return d; - } - if (d === '' || d === null) { - return d; - } - var flo = typeof d === 'number' ? d : parseFloat(d); - var negative = flo < 0 ? '-' : ''; - var abs = Math.abs(flo); - // Scientific notation for large and small numbers - if (abs >= 100000000000 || (abs < 0.0001 && abs !== 0)) { - var exp = flo.toExponential(precision).split(/e\+?/); - return exp[0] + ' x 10' + exp[1] + ''; - } - // If NaN then there isn't much formatting that we can do - just - // return immediately, escaping any HTML (this was supposed to - // be a number after all) - if (isNaN(flo)) { - return util.escapeHtml(d); - } - flo = flo.toFixed(precision); - var absPart = Math.abs(flo); - var intPart = Math.abs(parseInt(flo, 10)); - var floatPart = precision - ? decimal + - (absPart - intPart).toFixed(precision).substring(2) - : ''; - // If zero, then can't have a negative prefix - if (intPart === 0 && parseFloat(floatPart) === 0) { - negative = ''; - } - return (negative + - (prefix || '') + - intPart - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, thousands) + - floatPart + - (postfix || '')); - } - }; - }, - text: function () { - return { - display: util.escapeHtml, - filter: util.escapeHtml - }; - } -}; - -/** - * Column options that can be given to DataTables at initialisation time. - */ -const defaults$4 = { - ariaTitle: '', - cellType: 'td', - className: '', - contentPadding: '', - createdCell: null, - data: null, - defaultContent: null, - footer: null, - name: '', - orderable: true, - orderData: null, - orderDataType: 'std', - orderSequence: ['asc', 'desc', ''], - render: null, - search: null, - searchable: true, - title: null, - type: null, - visible: true, - width: null -}; - -/** - * Internal settings object used for individual columns. Instances are held in - * the setting object's `columns` array and contains all the information that - * DataTables needs about each individual column. - * - * Note that this object is related to the column defaults but this one is the - * internal data store for DataTables's cache of columns. It should NOT be - * manipulated outside of DataTables. Any configuration should be done through - * the initialisation options. + * Apply ARIA roles for the header / footer of a scrolling table + * @param element */ -class Settings { - constructor() { - /** - * Flag to indicate if HTML5 data attributes should be used as the data - * source for filtering or sorting. True is either are. - */ - this.attrSrc = false; - this.ariaTitle = ''; - /** - * The class to apply to all cells in the table's `tbody`` for the column - */ - this.className = null; - /** - * When DataTables calculates the column widths to assign to each column, it - * finds the longest string in each column and then constructs a temporary - * table and reads the widths from that. The problem with this is that "mmm" - * is much wider then "iiii", but the latter is a longer string - thus the - * calculation can go wrong (doing it properly and putting it into an DOM - * object and measuring that is horribly(!) slow). Thus as a "work around" - * we provide this option. It will append its value to the text that is - * found to be the longest string for the column - i.e. padding. - */ - this.contentPadding = null; - /** - * Property to read the value for the cells in the column from the data - * source array / object. If null, then the default content is used, if a - * function is given then the return from the function is used. - */ - this.data = null; - /** - * Allows a default value to be given for a column's data, and will be used - * whenever a null data source is encountered (this can be because mData is - * set to null, or because the data source itself is null). - */ - this.defaultContent = null; - /** - * Name for the column, allowing reference to the column by name as well as - * by index (needs a lookup to work by name). - */ - this.name = null; - /** - * A list of the columns that sorting should occur on when this column is - * sorted. That this property is an array allows multi-column sorting to be - * defined for a column (for example first name / last name columns would - * benefit from this). The values are integers pointing to the columns to be - * sorted on (typically it will be a single integer pointing at itself, but - * that doesn't need to be the case). - */ - this.orderData = []; - /** - * Custom sorting data type - defines which of the available plug-ins in - * afnSortData the custom sorting will use - if any is defined. - */ - this.orderDataType = 'std'; - /** - * Class to be applied to the header element when sorting on this column - */ - this.orderingClass = null; - /** - * Define the sorting directions that are applied to the column, in sequence - * as the column is repeatedly sorted upon - i.e. the first value is used as - * the sorting direction when the column if first sorted (clicked on). Sort - * it again (click again) and it will move on to the next index. Repeat - * until loop. - */ - this.orderSequence = []; - /** - * Partner property to mData which is used (only when defined) to get the - * data - i.e. it is basically the same as mData, but without the 'set' - * option, and also the data fed to it is the result from mData. This is the - * rendering method to match the data method of mData. - */ - this.render = null; - /** - * Title of the column - what is seen in the TH element (nTh). - */ - this.title = null; - /** - * Store for manual type assignment using the `column.type` option. This - * is held in store so we can manipulate the column's `type` property. - */ - this.typeManual = null; - /** Cached longest strings from a column */ - this.wideStrings = null; - /** - * Width of the column - */ - this.width = null; - /** - * Width of the column when it was first "encountered" - */ - this.widthOrig = null; +function scrollAria(element) { + if (element) { + element.find('tfoot:not([role])').attr('role', 'rowgroup'); + element.find('tr:not([role])').attr('role', 'row'); + element.find('th:not([role])').attr('role', 'columnheader'); + element.find('td:not([role])').attr('role', 'cell'); } } -const defaults$3 = { - boundary: false, - caseInsensitive: true, - columns: null, - exact: false, - regex: false, - return: false, - search: '', - smart: true -}; /** - * Create a new search options object + * Add the options to the page HTML for the table * - * @param parts Values to assign, otherwise the defaults will be used - * @returns New object + * @param ctx DataTables context */ -function create$2(parts = {}) { - return util.object.assignDeep({}, defaults$3, parts); +function createLayout(ctx) { + var classes = ctx.classes; + // Wrapper div around everything DataTables controls + var insert = Dom + .c('div') + .attr('id', ctx.tableId + '_wrapper') + .classAdd(classes.container) + .insertBefore(ctx.table); + ctx.tableWrapper = insert.get(0); + if (ctx.dom) { + // Legacy + legacyDom(ctx, ctx.dom, insert); + } + else { + var top = convert(ctx, ctx.layout, 'top'); + var bottom = convert(ctx, ctx.layout, 'bottom'); + var render = renderer(ctx, 'layout'); + // Everything above - the renderer will actually insert the contents into the document + top.forEach(function (item) { + render(ctx, insert, item); + }); + // The table - always the center of attention + render(ctx, insert, { + full: { + contents: [featureTable(ctx)], + items: [], + table: true + } + }); + // Everything below + bottom.forEach(function (item) { + render(ctx, insert, item); + }); + } + // Processing floats on top, so it isn't an inserted feature + processingHtml(ctx); } - -const browser = { - barWidth: -1, - scrollbarLeft: false -}; -const hungarianToCamelRe = /^(a|aa|ai|ao|as|b|fn|i|m|o|s)([A-Z])([a-z].*$)/; /** - * Take an object which has hungarian notation parameters and convert them to - * camelCase style. This is to allow compatibility with DataTables 1.9 and - * earlier which only used hungarian notation, and also with DataTables 1.10 - 2 - * which allowed it to be used. + * Expand the layout items into an object for the rendering function */ -function hungarianToCamel(user) { - if (!user) { - return user; +function layoutItems(row, align, items) { + if (Array.isArray(items)) { + for (var i = 0; i < items.length; i++) { + layoutItems(row, align, items[i]); + } + return; } - let userKeys = Object.keys(user); - let userAny = user; - for (let i = 0; i < userKeys.length; i++) { - let userKey = userKeys[i]; - let match = userKey.match(hungarianToCamelRe); - // Is the key in hungarian notation? - if (match) { - // If so map it down - user[match[2].toLowerCase() + match[3]] = userAny[userKey]; + var rowCell = row[align]; // can't be undefined - will have been created by getRow + // If it is an object, then there can be multiple features contained in it + if (util.is.plainObject(items)) { + // Is it an cell object already, with rowId, etc. A feature plugin cannot + // be named "features" due to this check + if (items.features) { + if (items.rowId) { + row.id = items.rowId; + } + if (items.rowClass) { + row.className = items.rowClass; + } + rowCell.id = items.id; + rowCell.className = items.className; + layoutItems(row, align, items.features); } - // Recurse down through the object - if (util.is.plainObject(userAny[userKey])) { - hungarianToCamel(userAny[userKey]); + else { + // An object of features and configuration options - e.g. `{paging: {startEnd: false}}` + util.object.each(items, (key, val) => { + rowCell.items.push({ + feature: key, + opts: val + }); + }); } } - return user; + else { + // Otherwise, it is a function, node or Dom / jQuery instance and can just get added + rowCell.items.push(items); + } } /** - * Map one parameter onto another + * Find, or create a layout row and setup a target cell in it * - * @param o Object to map - * @param newKey The new parameter name - * @param oldKey The old parameter name + * @param rows Rows array to search for the target row. Is mutated when a row is + * added if not found. + * @param rowNum Row index to get + * @param align Where the cell position is + * @returns The row */ -function compatMap(o, newKey, oldKey) { - if (o[oldKey] !== undefined) { - o[newKey] = o[oldKey]; +function getRow(rows, rowNum, align) { + var row; + // Find existing rows + for (var i = 0; i < rows.length; i++) { + row = rows[i]; + if (row.rowNum === rowNum) { + // full is on its own, but start and end share a row + if ((align === 'full' && row.full) || + ((align === 'start' || align === 'end') && + (row.start || row.end))) { + if (!row[align]) { + row[align] = { + contents: [], + items: [] + }; + } + return row; + } + } } + // If we get this far, then there was no match, create a new row + row = { + rowNum: rowNum + }; + row[align] = { + contents: [], + items: [] + }; + rows.push(row); + return row; } /** - * Provide backwards compatibility for the main DT options. Note that the new - * options are mapped onto the old parameters, so this is an external interface - * change only. + * Convert a `layout` object given by a user to the object structure needed + * for the renderer. This is done twice, once for above and once for below + * the table. Ordering must also be considered. * - * @param init Object to map + * @param settings DataTables settings object + * @param layout Layout object to convert + * @param side `top` or `bottom` + * @returns Converted array structure - one item for each row. */ -function compatOpts(init) { - // Convert any old style parameters to camelCase - hungarianToCamel(init); - // Map old parameter names to new - compatMap(init, 'ordering', 'sort'); - compatMap(init, 'orderMulti', 'sortMulti'); - compatMap(init, 'orderClasses', 'sortClasses'); - compatMap(init, 'orderCellsTop', 'sortCellsTop'); - compatMap(init, 'order', 'sorting'); - compatMap(init, 'orderFixed', 'sortingFixed'); - compatMap(init, 'paging', 'paginate'); - compatMap(init, 'pagingType', 'paginationType'); - compatMap(init, 'pageLength', 'displayLength'); - compatMap(init, 'searching', 'filter'); - compatMap(init, 'stateDuration', 'cookieDuration'); - // Boolean initialisation of x-scrolling - if (typeof init.scrollX === 'boolean') { - init.scrollX = init.scrollX ? '100%' : ''; - } - // Objects for ordering - if (typeof init.ordering === 'object') { - init.orderIndicators = - init.ordering.indicators !== undefined - ? init.ordering.indicators - : true; - init.orderHandler = - init.ordering.handler !== undefined ? init.ordering.handler : true; - init.ordering = true; - } - else if (init.ordering === false) { - init.orderIndicators = false; - init.orderHandler = false; - } - else if (init.ordering === true) { - init.orderIndicators = true; - init.orderHandler = true; - } - // Which cells are the title cells? - if (typeof init.orderCellsTop === 'boolean') { - init.titleRow = init.orderCellsTop; - } - // Column search objects are in an array, so it needs to be converted - // element by element - var searchCols = init.searchCols; - if (searchCols) { - for (var i = 0, iLen = searchCols.length; i < iLen; i++) { - if (searchCols[i]) { - hungarianToCamel(searchCols[i]); - } +function convert(settings, layout, side) { + var rows = []; + // Split out into an array + util.object.each(layout, function (pos, items) { + var parts = pos.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/); + if (items === null || !parts) { + return; } + var rowNum = parts[2] ? parseInt(parts[2]) : 0; + var align = parts[3] ? parts[3].toLowerCase() : 'full'; + // Filter out the side we aren't interested in + if (parts[1] !== side) { + return; + } + // Only really a type check + if (align !== 'full' && align !== 'start' && align !== 'end') { + return; + } + // Get or create the row we should attach to + var row = getRow(rows, rowNum, align); + layoutItems(row, align, items); + }); + // Order by item identifier + rows.sort(function (a, b) { + var order1 = a.rowNum || 0; + var order2 = b.rowNum || 0; + // If both in the same row, then the row with `full` comes first + if (order1 === order2) { + var ret = a.full && !b.full ? -1 : 1; + return side === 'bottom' ? ret * -1 : ret; + } + return order2 - order1; + }); + // Invert for below the table + if (side === 'bottom') { + rows.reverse(); } - // Enable search delay if server-side processing is enabled - if (init.serverSide && !init.searchDelay) { - init.searchDelay = 400; - } - // Language - if (init.language && init.language.url && !init.language.ajax) { - init.language.ajax = init.language.url; + for (var row = 0; row < rows.length; row++) { + delete rows[row].rowNum; + resolve(settings, rows[row]); } + return rows; +} +/** + * Convert the contents of a row's layout object to nodes that can be inserted + * into the document by a renderer. Execute functions, look up plug-ins, etc. + * + * @param settings DataTables settings object + * @param row Layout object for this row + */ +function resolve(settings, row) { + var getFeature = function (feature, opts) { + if (!ext.features[feature]) { + log(settings, 0, 'Unknown feature: ' + feature); + } + return ext.features[feature].apply(this, [settings, opts]); + }; + // Resolve items in the `contents` array from being an identifier, such as + // the name of a feature, into the node to display. + var resolve = function (item) { + if (!row[item]) { + return; + } + row[item].contents = row[item].items + .filter(item => !!item) + .map(item => { + if (typeof item === 'string') { + return getFeature(item, null); + } + else if (util.is.plainObject(item)) { + // If it's an object, it just has feature and opts properties from + // the transform in _layoutArray + return getFeature(item.feature, item.opts); + } + else if (typeof item.node === 'function') { + return item.node(settings); + } + else if (typeof item === 'function') { + var inst = item(settings); + return typeof inst.node === 'function' ? inst.node() : inst; + } + else if (item.nodeName) { + // An HTML element + return item; + } + else if (item instanceof Dom) { + return item.get(0); + } + else if (item.length) { + // Possibly jQuery + return item[0]; + } + }); + }; + resolve('start'); + resolve('end'); + resolve('full'); } /** - * Provide backwards compatibility for column options. Note that the new options - * are mapped onto the old parameters, so this is an external interface change - * only. + * Draw the table with the legacy DOM property * - * @param init Object to map + * @param settings DT settings instance + * @param layout DOM string + * @param insert Insert point */ -function compatCols(init) { - // Convert any old style parameters to camelCase - hungarianToCamel(init); - // typeof columnDefaults - compatMap(init, 'orderable', 'sortable'); - compatMap(init, 'orderData', 'dataSort'); - compatMap(init, 'orderSequence', 'sorting'); - compatMap(init, 'orderDataType', 'sortDataType'); - compatMap(init, 'className', 'class'); - // orderData can be given as an integer - var dataSort = init.aDataSort; - var orderData = init.orderData; - if (typeof dataSort === 'number') { - init.orderData = [dataSort]; - } - if (typeof orderData === 'number') { - init.orderData = [orderData]; - } - // Backwards compatibility for mDataProp from 1.9- - if (init.dataProp !== undefined && !init.data) { - init.data = init.dataProp; +function legacyDom(settings, layout, insert) { + let parts = layout.match(/(".*?")|('.*?')|./g); + let featureNode, option, newNode, next, attr; + if (!parts) { + return; } -} -/** - * Browser feature detection for capabilities, quirks - * - * @param ctx DataTables settings object - */ -function browserDetect(ctx) { - // We don't need to do this every time DataTables is constructed, the values - // calculated are specific to the browser and OS configuration which we - // don't expect to change between initialisations - if (browser.barWidth === -1) { - // Scrolling feature / quirks detection - var n = Dom - .c('div') - .css({ - position: 'fixed', - top: '0', - left: -1 * window.pageXOffset + 'px', // allow for scrolling - height: '1px', - width: '1px', - overflow: 'hidden' - }) - .append(Dom - .c('div') - .css({ - position: 'absolute', - top: '1px', - left: '1px', - width: '100px', - overflow: 'scroll' - }) - .append(Dom.c('div').css({ - width: '100%', - height: '10px' - }))) - .appendTo('body'); - var outer = n.children(); - var inner = outer.children(); - browser.barWidth = outer.get(0).offsetWidth - outer.get(0).clientWidth; - browser.scrollbarLeft = Math.round(inner.offset().left) !== 1; - n.remove(); + for (let i = 0; i < parts.length; i++) { + featureNode = null; + option = parts[i]; + if (option == '<') { + // New container div + newNode = Dom.c('div'); + // Check to see if we should append an id and/or a class name to the container + next = parts[i + 1]; + if (next[0] == "'" || next[0] == '"') { + attr = next.replace(/['"]/g, ''); + let id = '', className; + /* The attribute can be in the format of "#id.class", "#id" or "class" This logic + * breaks the string into parts and applies them as needed + */ + if (attr.indexOf('.') != -1) { + let split = attr.split('.'); + id = split[0]; + className = split[1]; + } + else if (attr[0] == '#') { + id = attr; + } + else { + className = attr; + } + newNode.attr('id', id.substring(1)).classAdd(className); + i++; // Move along the position array + } + insert.append(newNode.get()); // TODO + insert = newNode; + } + else if (option == '>') { + // End container div + insert = insert.parent(); + } + else if (option == 't') { + // Table + featureNode = featureTable(settings); + } + else { + ext.feature.forEach(function (feature) { + if (option == feature.cFeature) { + featureNode = feature.fnInit(settings); + } + }); + } + // Add to the display + if (featureNode) { + // TODO when doing the full dom update, won't need this check + insert.append(featureNode instanceof Dom ? featureNode.get() : featureNode); + } } - Object.assign(ctx.browser, browser); - ctx.scroll.barWidth = browser.barWidth; } -const defaults$2 = { - addedClasses: [], - cells: [], - data: [], - details: undefined, - detailsShow: undefined, - displayData: null, - idx: -1, - orderCache: null, - searchCellCache: null, - searchRowCache: null, - src: 'dom', - tr: null -}; -/** - * Create a new object that is a row model - * - * @param parts Values to assign, otherwise the defaults will be used - * @returns New object - */ -function create$1(parts = {}) { - return util.object.assignDeep({}, defaults$2, parts); +function sortInit(settings) { + var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])'; + if (settings.orderHandler) { + columnOrderingCells(settings, notSelector) + .each(el => { + sortAttachListener(settings, el, ''); + }); + } + // Need to resolve the user input array into our internal structure + var order = []; + sortResolve(settings, order, settings.order); + settings.order = order; } - /** - * Add a data array to the table, creating DOM node etc. This is the parallel to - * gatherData, but for adding rows from a JavaScript source, rather than a - * DOM source. + * Attach event listeners to a node that will trigger ordering on a column * - * @param settings DataTables settings object - * @param dataIn data array to be added - * @param tr TR element to add to the table - optional. If not given, DataTables - * will create a row automatically - * @param tds Array of TD|TH elements for the row - must be given if tr is. - * @returns >=0 if successful (index of new data entry), -1 if failed + * @param settings DataTables context + * @param node Node to attach to + * @param selector Delegate selector + * @param column Column index to target + * @param callback Callback for when done */ -function addData(settings, dataIn, tr, tds) { - /* Create the object for storing information about this new row */ - var rowIdx = settings.data.length; - var row = create$1({ - src: tr ? 'dom' : 'data', - idx: rowIdx +function sortAttachListener(settings, node, selector, column, callback) { + bindAction(node, selector, function (e) { + var run = false; + var columns = column === undefined + ? columnsFromHeader(e.target) + : typeof column === 'function' + ? column() + : Array.isArray(column) + ? column + : [column]; + if (columns.length) { + for (var i = 0, iLen = columns.length; i < iLen; i++) { + var ret = sortAdd(settings, columns[i], i, e.shiftKey); + if (ret !== false) { + run = true; + } + // If the first entry is no sort, then subsequent + // sort columns are ignored + if (settings.order.length === 1 && + settings.order[0][1] === '') { + break; + } + } + if (run) { + processingRun(settings, true, function () { + sort(settings); + sortDisplay(settings, settings.display); + reDraw(settings, false, false); + if (callback) { + callback(); + } + }); + } + } }); - row.data = dataIn; - settings.data.push(row); - var columns = settings.columns; - for (var i = 0, iLen = columns.length; i < iLen; i++) { - // Invalidate the column types as the new data needs to be revalidated - columns[i].type = null; - } - /* Add to the display array */ - settings.displayMaster.push(rowIdx); - var id = settings.rowIdFn(dataIn); - if (id !== undefined) { - settings.ids[id] = row; - } - /* Create the DOM information, or register it if already present */ - if (tr || !settings.features.deferRender) { - createTr(settings, rowIdx, tr, tds); - } - return rowIdx; } /** - * Add one or more TR elements to the table. Generally we'd expect to - * use this for reading data from a DOM sourced table, but it could be - * used for an TR element. Note that if a TR is given, it is used (i.e. - * it is not cloned). + * Sort the display array to match the master's order * - * @param settings DataTables settings object - * @param rows The TR element(s) to add to the table - * @returns Array of indexes for the added rows + * @param settings DataTables context + * @param display The display array */ -function addTr(settings, rows) { - return rows.mapTo(el => { - let row = getRowElementsFromNode(settings, el); - return addData(settings, row.data, el, row.cells); +function sortDisplay(settings, display) { + if (display.length < 2) { + return; + } + var master = settings.displayMaster; + var masterMap = {}; + var map = {}; + var i; + // Rather than needing an `indexOf` on master array, we can create a map + for (i = 0; i < master.length; i++) { + masterMap[master[i]] = i; + } + // And then cache what would be the indexOf from the display + for (i = 0; i < display.length; i++) { + map[display[i]] = masterMap[display[i]]; + } + display.sort(function (a, b) { + // Short version of this function is simply `master.indexOf(a) - master.indexOf(b);` + return map[a] - map[b]; }); } /** - * Get the data for a given cell from the internal cache, taking into account - * data mapping + * Convert the API variants that can be used for defining the order into our + * internal OrderColumn array. * - * @param settings DataTables settings object - * @param rowIdx data row id - * @param colIdx Column index - * @param type data get type ('display', 'type' 'filter|search' 'sort|order') - * @returns Cell data + * @param settings DataTable context object + * @param nestedSort Array to write the resolve values to + * @param sortItem Source object / array from user (It is really an `Order` + * but due to `aaSorting` being used for input and the internal structure + * it is currently any). + * @todo Split aaSorting into unresolved and resolved parameters (in state.ts as + * well) */ -function getCellData(settings, rowIdx, colIdx, type) { - if (type === 'search') { - type = 'filter'; - } - else if (type === 'order') { - type = 'sort'; - } - var row = settings.data[rowIdx]; - if (!row) { - return undefined; +function sortResolve(settings, nestedSort, sortItem // TODO typing +) { + var push = function (a) { + if (plainObject(a)) { + let orderIdx = a; + let orderName = a; + if (orderIdx.idx !== undefined) { + // Index based ordering + nestedSort.push([orderIdx.idx, orderIdx.dir]); + } + else if (orderName.name) { + // Name based ordering + var cols = pluck(settings.columns, 'name'); + var idx = cols.indexOf(orderName.name); + if (idx !== -1) { + nestedSort.push([idx, orderName.dir]); + } + } + } + else { + // Plain column index and direction pair + nestedSort.push(a); + } + }; + if (plainObject(sortItem)) { + // Object + push(sortItem); } - var draw = settings.drawCount; - var col = settings.columns[colIdx]; - var rowData = row.data; - var defaultContent = col.defaultContent; - var cellData = col.dataGet(rowData, type, { - settings: settings, - row: rowIdx, - col: colIdx - }); - // Allow for a node being returned for non-display types - if (type !== 'display' && - cellData && - typeof cellData === 'object' && - cellData.nodeName) { - cellData = cellData.innerHTML; + else if (Array.isArray(sortItem) && typeof sortItem[0] === 'number') { + // 1D array + push(sortItem); } - if (cellData === undefined) { - if (settings.drawError != draw && defaultContent === null) { - log(settings, 0, 'Requested unknown parameter ' + - (typeof col.data == 'function' - ? '{function}' - : "'" + col.data + "'") + - ' for row ' + - rowIdx + - ', column ' + - colIdx, 4); - settings.drawError = draw; + else if (Array.isArray(sortItem)) { + // 2D array + for (var z = 0; z < sortItem.length; z++) { + push(sortItem[z]); // Object or array } - return defaultContent; } - // When the data source is null and a specific data type is requested (i.e. - // not the original data), we can use default column data - if ((cellData === rowData || cellData === null) && - defaultContent !== null && - type !== undefined) { - cellData = defaultContent; +} +function sortFlatten(settings) { + var i, k, kLen, aSort = [], extSort = ext.type.order, aoColumns = settings.columns, dataSort, colIdx, type, srcCol, fixed = settings.orderFixed, fixedObj = plainObject(fixed), nestedSort = []; + if (!settings.features.ordering) { + return aSort; } - else if (typeof cellData === 'function') { - // If the data source is a function, then we run it and use the return, - // executing in the scope of the data object (for instances) - return cellData.call(rowData); + // Build the sort array, with pre-fix and post-fix options if they have been + // specified + if (Array.isArray(fixed)) { + sortResolve(settings, nestedSort, fixed); } - if (cellData === null && type === 'display') { - return ''; + if (fixedObj && fixed.pre) { + sortResolve(settings, nestedSort, fixed.pre); } - if (type === 'filter') { - var formatters = ext.type.search; - if (col.type && formatters[col.type]) { - cellData = formatters[col.type](cellData); + sortResolve(settings, nestedSort, settings.order); + if (fixedObj && fixed.post) { + sortResolve(settings, nestedSort, fixed.post); + } + for (i = 0; i < nestedSort.length; i++) { + srcCol = nestedSort[i][0]; + if (aoColumns[srcCol]) { + dataSort = aoColumns[srcCol].orderData; + for (k = 0, kLen = dataSort.length; k < kLen; k++) { + colIdx = dataSort[k]; + type = aoColumns[colIdx].type || 'string'; + if (nestedSort[i]._idx === undefined) { + nestedSort[i]._idx = aoColumns[colIdx].orderSequence.indexOf(nestedSort[i][1]); + } + if (nestedSort[i][1]) { + aSort.push({ + src: srcCol, + col: colIdx, + dir: nestedSort[i][1], + index: nestedSort[i]._idx, + type: type, + formatter: extSort[type + '-pre'], + sorter: extSort[type + '-' + nestedSort[i][1]] + }); + } + } } } - return cellData; + return aSort; } /** - * Set the value for a specific cell, into the internal data cache + * Change the order of the table * - * @param settings DataTables settings object - * @param rowIdx data row id - * @param colIdx Column index - * @param val Value to set + * @param ctx DataTables settings object + * @param col Column to perform sort on + * @param dir Direction to sort on */ -function setCellData(settings, rowIdx, colIdx, val) { - let row = settings.data[rowIdx]; - if (row) { - let col = settings.columns[colIdx]; - let rowData = row.data; - col.dataSet(rowData, val, { - settings: settings, - row: rowIdx, - col: colIdx +function sort(ctx, col, dir) { + var i, iLen, aiOrig = [], extSort = ext.type.order, data = ctx.data, sortCol, displayMaster = ctx.displayMaster, aSort; + // Make sure the columns all have types defined + columnTypes(ctx); + // Allow a specific column to be sorted, which will _not_ alter the display + // master + if (col !== undefined) { + var srcCol = ctx.columns[col]; + aSort = [ + { + src: col, + col: col, + dir: dir || '', + index: 0, + type: srcCol.type, + formatter: extSort[srcCol.type + '-pre'], + sorter: extSort[srcCol.type + '-' + dir] + } + ]; + displayMaster = displayMaster.slice(); + } + else { + aSort = sortFlatten(ctx); + } + for (i = 0, iLen = aSort.length; i < iLen; i++) { + sortCol = aSort[i]; + // Load the data needed for the sort, for each cell + sortData(ctx, sortCol.col); + } + /* No sorting required if server-side or no sorting array */ + if (dataSource(ctx) != 'ssp' && aSort.length !== 0) { + // Reset the initial positions on each pass so we get a stable sort + for (i = 0, iLen = displayMaster.length; i < iLen; i++) { + aiOrig[i] = i; + } + // If the first sort is desc, then reverse the array to preserve original + // order, just in reverse + if (aSort.length && aSort[0].dir === 'desc' && ctx.orderDescReverse) { + aiOrig.reverse(); + } + /* Do the sort - here we want multi-column sorting based on a given data source (column) + * and sorting function (from oSort) in a certain direction. It's reasonably complex to + * follow on its own, but this is what we want (example two column sorting): + * fnLocalSorting = function(a,b){ + * var test; + * test = oSort['string-asc']('data11', 'data12'); + * if (test !== 0) + * return test; + * test = oSort['numeric-desc']('data21', 'data22'); + * if (test !== 0) + * return test; + * return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); + * } + * Basically we have a test for each sorting column, if the data in that column is equal, + * test the next column. If all columns match, then we use a numeric sort on the row + * positions in the original data array to provide a stable sort. + */ + displayMaster.sort(function (a, b) { + var _a, _b; + var x, y, k, test, sortItem, len = aSort.length, dataA = (_a = data[a]) === null || _a === void 0 ? void 0 : _a.orderCache, dataB = (_b = data[b]) === null || _b === void 0 ? void 0 : _b.orderCache; + for (k = 0; k < len; k++) { + sortItem = aSort[k]; + // Data, which may have already been through a `-pre` function + x = dataA[sortItem.col]; + y = dataB[sortItem.col]; + if (sortItem.sorter) { + // If there is a custom sorter (`-asc` or `-desc`) for this + // data type, use it + test = sortItem.sorter(x, y); + if (test !== 0) { + return test; + } + } + else { + // Otherwise, use generic sorting + test = x < y ? -1 : x > y ? 1 : 0; + if (test !== 0) { + return sortItem.dir === 'asc' ? test : -test; + } + } + } + x = aiOrig[a]; + y = aiOrig[b]; + return x < y ? -1 : x > y ? 1 : 0; }); } -} -/** - * Write a value to a cell - * - * @param td Cell - * @param val Value - */ -function writeCell(td, val) { - let cell = Dom.s(td); - if (val && typeof val === 'object' && val.nodeName) { - cell.empty().append(val); + else if (aSort.length === 0) { + // Apply index order + displayMaster.sort(function (x, y) { + return x < y ? -1 : x > y ? 1 : 0; + }); } - else { - cell.html(val); + if (col === undefined) { + // Tell the draw function that we have sorted the data + ctx.wasOrdered = true; + ctx.sortDetails = aSort; + callbackFire(ctx, null, 'order', [ctx, aSort]); } + return displayMaster; } /** - * Return an array with the full table data - * - * @param settings DataTables settings object - * @returns array {array} aData Master data array - */ -function getDataMaster(settings) { - return util.array.pluck(settings.data, 'data'); -} -/** - * Nuke the table - * - * @param settings DataTables settings object - */ -function clearTable(settings) { - settings.data.length = 0; - settings.displayMaster.length = 0; - settings.display.length = 0; - settings.ids = {}; -} -/** - * Mark cached data as invalid such that a re-read of the data will occur when - * the cached data is next requested. Also update from the data source object. + * Function to run on user sort request * - * @param settings DataTables settings object - * @param rowIdx Row index to invalidate - * @param src Source to invalidate from: undefined, 'auto', 'dom' or 'data' - * @param colIdx Column index to invalidate. If undefined the whole row will be - * invalidated + * @param settings dataTables settings object + * @param colIdx column sorting index + * @param addIndex Counter + * @param shift Shift click add */ -function invalidateRow(settings, rowIdx, src, colIdx) { - var row = settings.data[rowIdx]; - var i, iLen; - if (!row) { - return; +function sortAdd(settings, colIdx, addIndex, shift) { + var col = settings.columns[colIdx]; + var sorting = settings.order; + var asSorting = col.orderSequence; + var nextSortIdx; + var next = function (a, overflow) { + var idx = a._idx; + if (idx === undefined) { + idx = asSorting.indexOf(a[1]); + } + return idx + 1 < asSorting.length ? idx + 1 : overflow ? null : 0; + }; + if (!col.orderable) { + return false; } - // Remove the cached data for the row - row.orderCache = null; - row.searchCellCache = null; - row.displayData = null; - // Are we reading last data from DOM or the data object? - if (src === 'dom' || ((!src || src === 'auto') && row.src === 'dom')) { - // Read the data from the DOM - row.data = getRowElementsFromModel(settings, row, colIdx).data; + // Convert to 2D array if needed + if (typeof sorting[0] === 'number') { + sorting = settings.order = [sorting]; } - else { - // Reading from data object, update the DOM - var cells = row.cells; - var display = getRowDisplay(settings, rowIdx); - if (cells.length) { - if (colIdx !== undefined) { - writeCell(cells[colIdx], display[colIdx]); + // If appending the sort then we are multi-column sorting + if ((shift || addIndex) && settings.features.orderMulti) { + // Are we already doing some kind of sort on this column? + var sortIdx = pluck(sorting, '0').indexOf(colIdx); + if (sortIdx !== -1) { + // Yes, modify the sort + nextSortIdx = next(sorting[sortIdx], true); + if (nextSortIdx === null && sorting.length === 1) { + nextSortIdx = 0; // can't remove sorting completely + } + if (nextSortIdx === null || asSorting[nextSortIdx] === '') { + sorting.splice(sortIdx, 1); } else { - for (i = 0, iLen = cells.length; i < iLen; i++) { - writeCell(cells[i], display[i]); - } + sorting[sortIdx][1] = asSorting[nextSortIdx]; + sorting[sortIdx]._idx = nextSortIdx; } } + else if (shift) { + // No sort on this column yet, being added by shift click + // add it as itself + sorting.push([colIdx, asSorting[0], 0]); + sorting[sorting.length - 1]._idx = 0; + } + else { + // No sort on this column yet, being added from a colspan + // so add with same direction as first column + sorting.push([colIdx, sorting[0][1], 0]); + sorting[sorting.length - 1]._idx = 0; + } } - invalidColumn(settings, colIdx); - // Update DataTables special `DT_*` attributes for the row - rowAttributes(settings, row); - callbackFire(settings, null, 'rowInvalidate', [settings, rowIdx, colIdx], false); -} -/** - * Column specific invalidation - * - * @param settings DataTables settings object - * @param colIdx Column index to invalidate, or all columns if not given - */ -function invalidColumn(settings, colIdx) { - // Column specific invalidation - var cols = settings.columns; - if (colIdx !== undefined) { - // Type - the data might have changed - cols[colIdx].type = null; - // Max length string. Its a fairly cheep recalculation, so not worth - // something more complicated - cols[colIdx].wideStrings = null; - } - else { - for (let i = 0, iLen = cols.length; i < iLen; i++) { - cols[i].type = null; - cols[i].wideStrings = null; + else if (sorting.length && sorting[0][0] == colIdx) { + // Single column - already sorting on this column, modify the sort + nextSortIdx = next(sorting[0]); + if (nextSortIdx) { + sorting.length = 1; + sorting[0][1] = asSorting[nextSortIdx]; + sorting[0]._idx = nextSortIdx; + } + else { + sorting.length = 1; + sorting[0][1] = asSorting[0]; + sorting[0]._idx = 0; } } - settings.containerWidth = -1; -} -/** - * Get the cells and data for a given row - from a element - * - * @param settings DataTables settings object - * @param row TR element from which to read data or existing row object from - * which to re-read the data from the cells - */ -function getRowElementsFromNode(settings, row) { - let data = settings.rowReadObject ? {} : []; - let cells = Dom.s(row).children('th, td'); - let id = row.getAttribute('id'); - cells.each((el, idx) => { - readCellData(settings, el, data, idx); - }); - if (id) { - util.set(settings.rowId)(data, id); + else { + // Single column - sort only on this column + sorting.length = 0; + sorting.push([colIdx, asSorting[0]]); + sorting[0]._idx = 0; } - return { - data: data, - cells: cells.get() - }; } /** - * Get the cells and data for a given row - from an existing row model + * Set the sorting classes on table's body, Note: it is safe to call this function + * when bSort and bSortClasses are false * * @param settings DataTables settings object - * @param row Existing row object from which to re-read the data from the cells - * @param colIdx Optional column index */ -function getRowElementsFromModel(settings, row, colIdx) { - let tds = row.cells; - for (let i = 0; i < tds.length; i++) { - if (colIdx === undefined || colIdx === i) { - readCellData(settings, tds[i], row.data, i); +function sortingClasses(settings) { + var oldSort = settings.lastOrder; + var sortClass = settings.classes.order.position; + var sortFlat = sortFlatten(settings); + var features = settings.features; + var i, iLen, colIdx; + if (features.ordering && features.orderClasses) { + // Remove old sorting classes + for (i = 0, iLen = oldSort.length; i < iLen; i++) { + colIdx = oldSort[i].src; + // Remove column sorting + Dom.s(pluck(settings.data, 'cells', colIdx)).classRemove(sortClass + (i < 2 ? i + 1 : 3)); } - } - // Read the ID from the DOM if present - if (row.tr) { - let id = row.tr.getAttribute('id'); - if (id) { - util.set(settings.rowId)(row.data, id); + // Add new column sorting + for (i = 0, iLen = sortFlat.length; i < iLen; i++) { + colIdx = sortFlat[i].src; + Dom.s(pluck(settings.data, 'cells', colIdx)).classAdd(sortClass + (i < 2 ? i + 1 : 3)); } } - return { - data: row.data, - cells: tds - }; + settings.lastOrder = sortFlat; } /** - * Read data from a cell into the data source object + * Get the data to sort a column, be it from cache, fresh (populating the + * cache), or from a sort formatter * * @param settings DataTables settings object - * @param cell The HTML cell element to read from - * @param data Data object / array to store data into - * @param colIdx The column index for the cell + * @param colIdx Column index */ -function readCellData(settings, cell, data, colIdx) { - let column = settings.columns[colIdx]; - let contents = cell.innerHTML.trim(); - if (column.attrSrc) { - // If we are working with attributes from the cell as values - let dataPoint = column.data; - let setter = util.set(dataPoint._); - let attr = function (str, cell) { - if (typeof str === 'string') { - let idx = str.indexOf('@'); - if (idx !== -1) { - let att = str.substring(idx + 1); - let setter = util.set(str); - setter(data, cell.getAttribute(att)); - } - } - }; - setter(data, contents); - attr(dataPoint.sort, cell); - attr(dataPoint.type, cell); - attr(dataPoint.filter, cell); +function sortData(settings, colIdx) { + // Custom sorting function - provided by the sort data type + var column = settings.columns[colIdx]; + var customSort = ext.order[column.orderDataType]; + var customData; + if (customSort) { + customData = customSort.call(settings.instance, settings, colIdx, columnIndexToVisible(settings, colIdx)); } - else { - if (!column.setter) { - // Cache the setter function - column.setter = util.set(column.data); + // Use / populate cache + var row, cellData; + var formatter = ext.type.order[column.type + '-pre']; + var data = settings.data; + for (var rowIdx = 0; rowIdx < data.length; rowIdx++) { + // Sparse array + if (!data[rowIdx]) { + continue; + } + row = data[rowIdx]; + if (row && !row.orderCache) { + row.orderCache = []; + } + if (row && (!row.orderCache[colIdx] || customSort)) { + cellData = customSort + ? customData[rowIdx] // If there was a custom sort function, use data from there + : getCellData(settings, rowIdx, colIdx, 'sort'); + row.orderCache[colIdx] = formatter + ? formatter(cellData, settings) + : cellData; } - column.setter(data, contents); } } +const defaults$3 = { + boundary: false, + caseInsensitive: true, + columns: null, + exact: false, + regex: false, + return: false, + search: '', + smart: true +}; /** - * Recalculate the column widths, if needed (by a column having been - * invalidated) + * Create a new search options object * - * @param settings DataTables settings object + * @param parts Values to assign, otherwise the defaults will be used + * @returns New object */ -function columnWidths(settings) { - if (settings.columns.map(c => c.wideStrings).includes(null)) { - calculateColumnWidths(settings); - } +function create$1(parts = {}) { + return util.object.assignDeep({}, defaults$3, parts); } + /** - * Calculate the width of columns for the table + * Alter the display settings to change the page * * @param settings DataTables settings object + * @param action Paging action to take: "first", "previous", "next" or "last" or + * page number to jump to (integer) + * @param redraw Automatically draw the update or not + * @returns true page has changed, false - no change */ -function calculateColumnWidths(settings) { - // Not interested in doing column width calculation if auto-width is disabled - if (!settings.features.autoWidth) { - return; - } - var table = settings.table, columns = settings.columns, scroll = settings.scroll, scrollY = scroll.y, scrollX = scroll.x, visibleColumns = getColumns(settings, 'visible'), tableWidthAttr = table.getAttribute('width'), // from DOM element - tableContainer = table.parentElement, i, j, column, columnIdx; - var styleWidth = table.style.width; - var containerWidth = wrapperWidth(settings); - // Don't re-run for the same width as the last time - if (containerWidth === settings.containerWidth) { - return false; - } - settings.containerWidth = containerWidth; - // If there is no width applied as a CSS style or as an attribute, we assume that - // the width is intended to be 100%, which is usually is in CSS, but it is very - // difficult to correctly parse the rules to get the final result. - if (!styleWidth && !tableWidthAttr) { - table.style.width = '100%'; - styleWidth = '100%'; - } - if (styleWidth && styleWidth.indexOf('%') !== -1) { - tableWidthAttr = styleWidth; - } - // Let plug-ins know that we are doing a recalc, in case they have changed any of the - // visible columns their own way (e.g. Responsive uses display:none). - callbackFire(settings, null, 'column-calc', [{ visible: visibleColumns }], false); - // Construct a worst case table with the widest, assign any user defined - // widths, then insert it into the DOM and allow the browser to do all - // the hard work of calculating table widths - var tmpTable = Dom - .s(table.cloneNode()) - .css('visibility', 'hidden') - .css('margin', '0') - .attrRemove('id'); - // Clean up the table body - tmpTable.append(Dom.c('tbody')); - // Clone the table header and footer - we can't use the header / footer - // from the cloned table, since if scrolling is active, the table's - // real header and footer are contained in different table tags - tmpTable - .append(settings.thead.cloneNode(true)) - .append(settings.tfoot.cloneNode(true)); - // Remove any assigned widths from the footer (from scrolling) - tmpTable.find('tfoot th, tfoot td').css('width', ''); - // Apply custom sizing to the cloned header - tmpTable.find('thead th, thead td').each(cell => { - // Get the `width` from the header layout - var width = columnsSumWidth(settings, cell, true); - if (width) { - cell.style.width = width; - // For scrollX we need to force the column width otherwise the - // browser will collapse it. If this width is smaller than the - // width the column requires, then it will have no effect - if (scrollX) { - cell.style.minWidth = width; - Dom.s(cell).append(Dom.c('div').css({ - width: width, - margin: '0', - padding: '0', - border: '0', - height: '1px' - })); - } - } - else { - cell.style.width = ''; - } - }); - // Get the widest strings for each of the visible columns and add them to - // our table to create a "worst case" - var longestData = []; - for (i = 0; i < visibleColumns.length; i++) { - longestData.push(getWideStrings(settings, visibleColumns[i])); - } - if (longestData.length) { - for (i = 0; i < longestData[0].length; i++) { - var tr = Dom.c('tr').appendTo(tmpTable.find('tbody')); - for (j = 0; j < visibleColumns.length; j++) { - columnIdx = visibleColumns[j]; - column = columns[columnIdx]; - var longest = longestData[j][i] || ''; - var autoClass = ext.type.className[column.type]; - var padding = column.contentPadding || (scrollX ? '-' : ''); - var text = longest + padding; - var cell = Dom - .c('td') - .classAdd(autoClass) - .classAdd(column.className) - .appendTo(tr); - if (longest.indexOf('<') === -1 && - longest.indexOf('&') === -1) { - cell.text(text); - } - else { - cell.html(text); - } - } - } - } - // Tidy the temporary table - remove name attributes so there aren't - // duplicated in the dom (radio elements for example) - tmpTable.find('[name]').attrRemove('name'); - // Table has been built, attach to the document so we can work with it. - // A holding element is used, positioned at the top of the container - // with minimal height, so it has no effect on if the container scrolls - // or not. Otherwise it might trigger scrolling when it actually isn't - // needed - var holder = Dom - .c('div') - .css(scrollX || scrollY - ? { - position: 'absolute', - top: '0', - left: '0', - height: '1px', - right: '0', - overflow: 'hidden' - } - : {}) - .append(tmpTable) - .appendTo(tableContainer); - // When scrolling (X or Y) we want to set the width of the table as - // appropriate. However, when not scrolling leave the table width as it - // is. This results in slightly different, but I think correct behaviour - if (scrollX) { - tmpTable.css('width', 'auto').attrRemove('width'); - // If there is no width attribute or style, then allow the table to - // collapse - if (tmpTable.width() < tableContainer.clientWidth && tableWidthAttr) { - tmpTable.width(tableContainer.clientWidth); - } - } - else if (scrollY) { - tmpTable.width(tableContainer.clientWidth); - } - else if (tableWidthAttr) { - tmpTable.width(tableWidthAttr); - } - // Get the width of each column in the constructed table - var total = 0; - var bodyCells = tmpTable.find('tbody tr').eq(0).children(); - for (i = 0; i < visibleColumns.length; i++) { - // Use getBounding for sub-pixel accuracy, which we then want to round - // up! - var bounding = bodyCells.get(i).getBoundingClientRect().width; - // Total is tracked to remove any sub-pixel errors as the outerWidth - // of the table might not equal the total given here - total += bounding; - // Width for each column to use - columns[visibleColumns[i]].width = stringToCss(bounding); - } - table.style.width = stringToCss(total); - // Finished with the table - ditch it - holder.remove(); - // If there is a width attr, we want to attach an event listener which - // allows the table sizing to automatically adjust when the window is - // resized. Use the width attr rather than CSS, since we can't know if the - // CSS is a relative value or absolute - DOM read is always px. - if (tableWidthAttr) { - table.style.width = stringToCss(tableWidthAttr); - } - if ((tableWidthAttr || scrollX) && !settings.reszEvt) { - var resize = util.throttle(function () { - var newWidth = wrapperWidth(settings); - // Don't do it if destroying or the container width is 0 - if (!settings.destroying && newWidth !== 0) { - adjustColumnSizing(settings); - } - }); - // For browsers that support it (~2020 onwards for wide support) we can watch for the - // container changing width. - if (window.ResizeObserver) { - // This is a tricky beast - if the element is visible when `.observe()` is called, - // then the callback is immediately run. Which we don't want. If the element isn't - // visible, then it isn't run, but we want it to run when it is then made visible. - // This flag allows the above to be satisfied. - var first = Dom.s(settings.tableWrapper).isVisible(); - // Use an empty div to attach the observer so it isn't impacted by height changes - var resizer = Dom - .c('div') - .css({ - width: '100%', - height: '0' - }) - .classAdd('dt-autosize') - .appendTo(settings.tableWrapper); - settings.resizeObserver = new ResizeObserver(function (e) { - if (first) { - first = false; - } - else { - resize(); - } - }); - settings.resizeObserver.observe(resizer.get(0)); +function pageChange(settings, action, redraw) { + var start = settings.displayStart, len = settings.pageLength, records = recordsDisplay(settings); + if (records === 0 || len === -1) { + start = 0; + } + else if (typeof action === 'number') { + start = action * len; + if (start > records) { + start = 0; } - else { - // For old browsers, the best we can do is listen for a window - // resize - window.addEventListener('resize', resize); - settings.windowResizeCb = resize; // For removal in `destroy` + } + else if (action == 'first') { + start = 0; + } + else if (action == 'previous') { + start = len >= 0 ? start - len : 0; + if (start < 0) { + start = 0; } - settings.reszEvt = true; } + else if (action == 'next') { + if (start + len < records) { + start += len; + } + } + else if (action == 'last') { + start = Math.floor((records - 1) / len) * len; + } + else if (action === 'ellipsis') { + return; + } + else { + log(settings, 0, 'Unknown paging action: ' + action, 5); + } + var changed = settings.displayStart !== start; + settings.displayStart = start; + callbackFire(settings, null, changed ? 'page' : 'page-nc', [settings]); + if (changed && redraw) { + draw(settings); + } + return changed; } + /** - * Get the width of the DataTables wrapper element + * State information for a table * * @param settings DataTables settings object - * @returns Width */ -function wrapperWidth(settings) { - let wrapper = Dom.s(settings.tableWrapper); - return wrapper.isVisible() ? wrapper.width() : 0; +function saveState(settings) { + if (settings.loadingState) { + return; + } + // Sort state saving uses [[idx, order]] structure. + var sorting = []; + sortResolve(settings, sorting, settings.order); + /* Store the interesting variables */ + var columns = settings.columns; + var state = { + columns: settings.columns.map(function (col, i) { + return { + name: col.name, + visible: col.visible, + search: Object.assign({}, settings.searches[i]) + }; + }), + length: settings.pageLength, + order: sorting.map(function (sort) { + // If a column name is available, use it + return columns[sort[0]] && columns[sort[0]].name + ? [columns[sort[0]].name, sort[1]] + : sort.slice(); + }), + search: Object.assign({}, settings.searches['*']), + searchGroups: Object.keys(settings.searches) + .filter(c => c.includes(',')) // Limit to only multi-column subsets + .map(c => Object.assign({}, settings.searches[c])), + start: settings.displayStart, + time: +new Date() + }; + settings.stateSaved = state; + callbackFire(settings, 'stateSaveParams', 'stateSaveParams', [ + settings, + state + ]); + if (settings.features.stateSave && !settings.destroying) { + settings.stateSaveCallback.call(settings.instance, settings, state); + } } /** - * Get the widest strings for each column. - * - * It is very difficult to determine what the widest string actually is due to variable character - * width and kerning. Doing an exact calculation with the DOM or even Canvas would kill performance - * and this is a critical point, so we use two techniques to determine a collection of the longest - * strings from the column, which will likely contain the widest strings: - * - * 1) Get the top three longest strings from the column - * 2) Get the top three widest words (i.e. an unbreakable phrase) + * Attempt to load a saved table state * - * @param settings DataTables settings object - * @param colIdx column of interest - * @returns Array of the longest strings + * @param settings dataTables settings object + * @param callback Callback to execute when the state has been loaded */ -function getWideStrings(settings, colIdx) { - var column = settings.columns[colIdx]; - // Do we need to recalculate (i.e. was invalidated), or just use the cached data? - if (!column.wideStrings) { - var allStrings = []; - var collection = []; - // Create an array with the string information for the column - for (var i = 0, iLen = settings.displayMaster.length; i < iLen; i++) { - var rowIdx = settings.displayMaster[i]; - var data = getRowDisplay(settings, rowIdx)[colIdx]; - var cellString = data && typeof data === 'object' && data.nodeType - ? data.innerHTML - : data + ''; - // Remove id / name attributes from elements so they - // don't interfere with existing elements - cellString = cellString - .replace(/id=".*?"/g, '') - .replace(/name=".*?"/g, ''); - // Don't want script, dialog or template tags in the width - // calculations as they are hidden content - cellString = cellString - .replace(/]*)?>/gi, ' ') - .replace(/]*)?>/gi, ' ') - .replace(/]*)?>/gi, ' '); - var noHtml = util.string - .stripHtml(cellString, ' ') - .replace(/ /g, ' '); - collection.push({ - str: cellString, - len: noHtml.length - }); - allStrings.push(noHtml); +function loadState(settings, callback) { + if (!settings.features.stateSave) { + callback(); + return; + } + var loaded = function (state, ignoreTime = false) { + implementState(settings, state, ignoreTime, callback); + }; + var state = settings.stateLoadCallback.call(settings.instance, settings, loaded); + if (state !== undefined) { + implementState(settings, state, false, callback); + } + // otherwise, wait for the loaded callback to be executed + return true; +} +function implementState(settings, s, ignoreTime, callback) { + var i, iLen; + var columns = settings.columns; + var currentNames = pluck(settings.columns, 'name'); + settings.loadingState = true; + // When StateRestore was introduced the state could now be implemented at + // any time Not just initialisation. To do this an api instance is required + // in some places + var api = settings.initDone ? new Api(settings) : null; + if (!ignoreTime) { + if (!s || !s.time) { + settings.loadingState = false; + callback(); + return; } - // Order and then cut down to the size we need - collection - .sort(function (a, b) { - return b.len - a.len; - }) - .splice(3); - column.wideStrings = collection.map(function (item) { - return item.str; - }); - // Longest unbroken string - const parts = allStrings.join(' ').split(' '); - parts.sort(function (a, b) { - return b.length - a.length; - }); - if (parts.length) { - column.wideStrings.push(parts[0]); + // Reject old data + var duration = settings.stateDuration; + if (duration > 0 && s.time < +new Date() - duration * 1000) { + settings.loadingState = false; + callback(); + return; } - if (parts.length > 1) { - column.wideStrings.push(parts[1]); + } + // Allow custom and plug-in manipulation functions to alter the saved data + // set and cancelling of loading by returning false + var abStateLoad = callbackFire(settings, 'stateLoadParams', 'stateLoadParams', [settings, s]); + if (abStateLoad.indexOf(false) !== -1) { + settings.loadingState = false; + callback(); + return; + } + // Store the saved state so it might be accessed at any time + settings.stateLoaded = assignDeep({}, s); + // This is needed for ColReorder, which has to happen first to allow all + // the stored indexes to be usable. It is not publicly documented. + callbackFire(settings, null, 'stateLoadInit', [settings, s], true); + // Page Length + if (s.length !== undefined) { + // If already initialised just set the value directly so that the select + // element is also updated + if (api) { + api.page.len(s.length); } - if (parts.length > 2) { - column.wideStrings.push(parts[3]); + else { + settings.pageLength = s.length; } } - return column.wideStrings; -} -/** - * Append a CSS unit (only if required) to a string - * - * @param s Value to css-ify - * @returns Value with css unit - */ -function stringToCss(s) { - if (s === null) { - return '0px'; + // Restore key features + if (s.start !== undefined) { + if (api === null) { + settings.displayStart = s.start; + settings.displayStartInit = s.start; + } + else { + pageChange(settings, s.start / settings.pageLength); + } + } + // Order + if (s.order !== undefined) { + settings.order = []; + for (let i = 0; i < s.order.length; i++) { + let col = s.order[i]; + let set = [col[0], col[1]]; + // A column name was stored and should be used for restore + if (typeof col[0] === 'string') { + // Find the name from the current list of column names + let idx = currentNames.indexOf(col[0]); + if (idx < 0) { + // If the column was not found ignore it and continue + continue; + } + set[0] = idx; + } + else if (set[0] >= columns.length) { + // If the column index is out of bounds ignore it and continue + continue; + } + settings.order.push(set); + } } - if (typeof s == 'number') { - return s < 0 ? '0px' : s + 'px'; + // Search + if (s.search !== undefined) { + Object.assign(settings.searches['*'], s.search); } - // Check it has a unit character already - return s.match(/\d$/) ? s + 'px' : s; -} -/** - * Re-insert the `col` elements for current visibility - * - * @param settings DT settings - */ -function colGroup(settings) { - var cols = settings.columns; - settings.colgroup.empty(); - for (var i = 0; i < cols.length; i++) { - if (cols[i].visible) { - settings.colgroup.append(cols[i].colEl); + if (s.searchGroups) { + s.searchGroups.forEach(group => { + if (group.columns) { + let index = group.columns.join(','); + settings.searches[index] = create$1(group); + } + }); + } + // Columns + if (s.columns) { + var set = s.columns; + var incoming = pluck(s.columns, 'name'); + // Check if it is a 2.2 style state object with a `name` property for + // the columns, and if the name was defined. If so, then create a new + // array that will map the state object given, to the current columns + // (don't bother if they are already matching tho). + if (incoming.join('').length && + incoming.join('') !== currentNames.join('')) { + set = []; + // For each column, try to find the name in the incoming array + for (i = 0; i < currentNames.length; i++) { + if (currentNames[i] != '') { + var idx = incoming.indexOf(currentNames[i]); + if (idx >= 0) { + set.push(s.columns[idx]); + } + else { + // No matching column name in the state's columns, so + // this might be a new column and thus can't have a + // state already. + set.push({}); + } + } + else { + // If no name, but other columns did have a name, then there + // is no knowing where this one came from originally so it + // can't be restored. + set.push({}); + } + } + } + // If the number of columns to restore is different from current, then + // all bets are off. + if (set.length === columns.length) { + for (i = 0, iLen = set.length; i < iLen; i++) { + var col = set[i]; + // Visibility + if (col.visible !== undefined) { + // If the api is defined, the table has been initialised so + // we need to use it rather than internal settings + if (api) { + // Don't redraw the columns on every iteration of this + // loop, we will do this at the end instead + api.column(i).visible(col.visible, false); + } + else { + columns[i].visible = col.visible; + } + } + // Search + if (col.search !== undefined) { + Object.assign(settings.searches[i], col.search); + // If out of order due to a change in order from named + // columns we need to make sure the index is correct + settings.searches[i].columns = [i]; + } + } + // If the api is defined then we need to adjust the columns once the + // visibility has been changed + if (api) { + api.one('draw', function () { + api.columns.adjust(); + }); + } } } + settings.loadingState = false; + callbackFire(settings, 'stateLoaded', 'stateLoaded', [settings, s]); + callback(); } /** - * Scrolling setup + * Draw the table for the first time, adding all required features * * @param settings DataTables settings object - * @returns Node to add to the DOM */ -function featureTable(settings) { - let table = Dom.s(settings.table); - let scroll = settings.scroll; - let scrollX = scroll.x; - let scrollY = scroll.y; - // No scrolling or x-scrolling only - if (scrollY === '' && scrollX === '') { - return table.get(0); - } - let classes = settings.classes.scrolling; - let caption = settings.captionNode; - let captionSide = caption - ? caption._captionSide - : null; - let tableCloneHeader = table.clone(false); - let tableCloneFooter = table.clone(false); - let footer = table.children('tfoot'); - let size = function (s) { - return !s ? '100%' : stringToCss(s); - }; - /* - * The HTML structure that we want to generate in this function is: - * div - scroller - * div - scroll head - * div - scroll head inner - * table - scroll head table - * thead - thead - * div - scroll body - * table - table (master table) - * thead - thead clone for sizing - * tbody - tbody - * div - scroll foot - * div - scroll foot inner - * table - scroll foot table - * tfoot - tfoot - */ - let scroller = Dom.c('div') - .classAdd(classes.container) - .attr('role', 'table') - .append(Dom.c('div') - .classAdd(classes.header.self) - .css({ - overflow: 'hidden', - position: 'relative', - border: '0', - width: scrollX ? size(scrollX) : '100%' - }) - .attr('role', 'none') - .append(Dom.c('div') - .classAdd(classes.header.inner) - .css({ - 'box-sizing': 'content-box', - width: scroll.xInner || '100%' - }) - .attr('role', 'none') - .append(tableCloneHeader - .attrRemove('id') - .css('margin-left', '0') - .append(captionSide === 'top' ? caption : null) - .append(table.children('thead'))))) - .append(Dom.c('div') - .classAdd(classes.body) - .css({ - position: 'relative', - overflow: 'auto', - width: size(scrollX) - }) - .attr('role', 'none') - .append(table)); - if (footer.count()) { - scroller.append(Dom.c('div') - .classAdd(classes.footer.self) - .css({ - overflow: 'hidden', - border: '0', - width: scrollX ? size(scrollX) : '100%' - }) - .attr('role', 'none') - .append(Dom.c('div') - .classAdd(classes.footer.inner) - .attr('role', 'none') - .append(tableCloneFooter - .attrRemove('id') - .css('margin-left', '0') - .append(captionSide === 'bottom' ? caption : null) - .append(table.children('tfoot'))))); +function initialise(settings) { + var i; + var init = settings.init; + var deferLoading = settings.deferLoading; + var dataSrc = dataSource(settings); + // Ensure that the table data is fully initialised + if (!settings.initialised) { + setTimeout(function () { + initialise(settings); + }, 200); + return; } - let children = scroller.children(); - let scrollHead = children.eq(0); - let scrollBody = children.eq(1); - let scrollFoot = children.eq(2); - // When the body is scrolled, then we also want to scroll the header and - // footer. Note that each element has its own scroll listener, and that in - // turn sets the scroll for the other elements. However this doesn't lead to - // an infinite loop as `scroll` is only triggered if the value changes. - scrollBody.on('scroll.DT', () => { - let scrollLeft = scrollBody.scrollLeft(); - scrollHead.scrollLeft(scrollLeft); - scrollFoot.scrollLeft(scrollLeft); - }); - scrollHead.on('scroll.DT', () => { - let scrollLeft = scrollHead.scrollLeft(); - scrollBody.scrollLeft(scrollLeft); - scrollFoot.scrollLeft(scrollLeft); - }); - scrollFoot.on('scroll.DT', () => { - let scrollLeft = scrollFoot.scrollLeft(); - scrollHead.scrollLeft(scrollLeft); - scrollBody.scrollLeft(scrollLeft); + // Build the header / footer for the table + buildHead(settings, 'header'); + buildHead(settings, 'footer'); + // Load the table's state (if needed) and then render around it and draw + loadState(settings, function () { + // Then draw the header / footer + drawHead(settings, settings.header); + drawHead(settings, settings.footer); + // Cache the paging start point, as the first redraw will reset it + var iAjaxStart = settings.displayStartInit; + // Local data load + // Check if there is data passing into the constructor + if (init && init.data) { + for (i = 0; i < init.data.length; i++) { + addData(settings, init.data[i]); + } + } + else if (deferLoading || dataSrc == 'dom') { + // Grab the data from the page + addTr(settings, Dom.s(settings.tbody).children('tr')); + } + // Filter not yet applied - copy the display master + settings.display = settings.displayMaster.slice(); + // Enable features + createLayout(settings); + sortInit(settings); + colGroup(settings); + /* Okay to show that something is going on now */ + processingDisplay(settings, true); + callbackFire(settings, null, 'preInit', [settings], true); + // If there is default sorting required - let's do it. The sort function + // will do the drawing for us. Otherwise we draw the table regardless of + // the Ajax source - this allows the table to look initialised for Ajax + // sourcing data (show 'loading' message possibly) + reDraw(settings); + // Server-side processing init complete is done by _fnAjaxUpdateDraw + if (dataSrc != 'ssp' || deferLoading) { + // if there is an ajax source load the data + if (dataSrc == 'ajax') { + buildAjax(settings, {}, function (json) { + var aData = ajaxDataSrc(settings, json, false); + // Got the data - add it to the table + for (i = 0; i < aData.length; i++) { + addData(settings, aData[i]); + } + // Reset the init display for cookie saving. We've already + // done a filter, and therefore cleared it before. So we + // need to make it appear 'fresh' + settings.displayStartInit = iAjaxStart; + reDraw(settings); + processingDisplay(settings, false); + initComplete(settings); + }); + } + else { + initComplete(settings); + processingDisplay(settings, false); + } + } }); - scrollBody.css('max-height', size(scrollY)); - if (!scroll.collapse) { - scrollBody.css('height', size(scrollY)); - } - settings.scrollHead = scrollHead; - settings.scrollBody = scrollBody; - settings.scrollFoot = scrollFoot; - // On redraw - align columns - settings.callbacks.draw.push(scrollDraw); - // Aria roles - because we break the table up into parts we need to be very - // explicit with the roles to create the accessability tree for the table, - // otherwise browser's attempt to "fix" the tree by filling in what it - // thinks are gaps. The static elements that we can assign roles to are done - // here. Dynamic ones are done in the draw function below. - table.attr('role', 'none'); - table.find('tbody').attr('role', 'rowgroup'); - tableCloneHeader.attr('role', 'none'); - tableCloneFooter.attr('role', 'none'); - settings.colgroup.find('colgroup').attr('role', 'none'); - // Move the info feature's aria desc by to the new "table" - let describedBy = table.attr('aria-describedby'); - if (describedBy) { - scroller.attr('aria-describedby', describedBy); - table.attrRemove('aria-describedby'); - } - return scroller.get(0); } /** - * Update the header, footer and body tables for resizing - i.e. column - * alignment. - * - * Welcome to the most horrible function DataTables. The process that this - * function follows is basically: - * 1. Re-create the table inside the scrolling div - * 2. Correct colgroup > col values if needed - * 3. Copy colgroup > col over to header and footer - * 4. Clean up + * Draw the table for the first time, adding all required features * * @param settings DataTables settings object */ -function scrollDraw(settings) { - // Given that this is such a monster function, a lot of variables are use - // to try and keep the minimised size as small as possible - let scroll = settings.scroll, barWidth = scroll.barWidth, divHeader = settings.scrollHead, divHeaderInner = divHeader.children('div'), divHeaderTable = divHeaderInner.children('table'), divBodyEl = settings.scrollBody, divBody = divBodyEl, divFooter = settings.scrollFoot, divFooterInner = divFooter.children('div'), divFooterTable = divFooterInner.children('table'), header = Dom.s(settings.thead), table = Dom.s(settings.table), footer = Dom.s(settings.tfoot), browser = settings.browser, headerCopy, footerCopy; - // If the scrollbar visibility has changed from the last draw, we need to - // adjust the column sizes as the table width will have changed to account - // for the scrollbar - let scrollBarVis = divBodyEl.get(0).scrollHeight > divBodyEl.get(0).clientHeight; - if (settings.scrollBarVis !== scrollBarVis && - settings.scrollBarVis !== undefined) { - settings.scrollBarVis = scrollBarVis; - adjustColumnSizing(settings); - return; // adjust column sizing will call this function again - } - else { - settings.scrollBarVis = scrollBarVis; +function initComplete(settings) { + if (settings.initDone) { + return; } - header.find('thead').attr('role', 'rowgroup'); - footer.find('tfoot').attr('role', 'rowgroup'); - // 1. Re-create the table inside the scrolling div - // Remove the old minimised thead and tfoot elements in the inner table - table.children('thead, tfoot').remove(); - // Clone the current header and footer elements and then place it into the - // inner table - headerCopy = header.clone(true).prependTo(table); - headerCopy.find('th, td').attrRemove('tabindex'); - headerCopy.find('[id]').attrRemove('id'); - if (footer.count()) { - footerCopy = footer.clone(true).prependTo(table); - footerCopy.find('[id]').attrRemove('id'); + var args = [settings, settings.json]; + settings.initDone = true; + // If the footer element is empty after initialisation, then remove it + let tfoot = Dom.s(settings.tfoot); + if (tfoot.children().count() === 0) { + tfoot.remove(); } - // 2. Correct colgroup > col values if needed - // It is possible that the cell sizes are smaller than the content, so we need to - // correct colgroup>col for such cases. This can happen if the auto width detection - // uses a cell which has a longer string, but isn't the widest! For example - // "Chief Executive Officer (CEO)" is the longest string in the demo, but - // "Systems Administrator" is actually the widest string since it doesn't collapse. - // Note the use of translating into a column index to get the `col` element. This - // is because of Responsive which might remove `col` elements, knocking the alignment - // of the indexes out. - if (settings.display.length) { - // Get the column sizes from the first row in the table. This should really be a - // [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year - // browser support - let firstTr = null; - let start = dataSource(settings) !== 'ssp' ? settings.displayStart : 0; - for (let i = start; i < start + settings.display.length; i++) { - let idx = settings.display[i]; - let row = settings.data[idx]; - if (row) { - let tr = row.tr; - if (tr) { - firstTr = tr; - break; - } + // Table is fully set up and we have data, so calculate the + // column widths + adjustColumnSizing(settings); + callbackFire(settings, null, 'plugin-init', args, true); + callbackFire(settings, 'init', 'init', args, true); +} + +/** + * Create an Ajax call based on the table's settings, taking into account that + * parameters can have multiple forms, and backwards compatibility. + * + * @param settings DataTables settings object + * @param data Data to send to the server, required by DataTables - may be + * augmented by developer callbacks + * @param fn Callback function to run when data is obtained + */ +function buildAjax(settings, data, fn) { + var ajaxData; + var ajaxConfig = settings.ajax; + var instance = settings.instance; + var callback = function (json) { + var status = settings.jqXHR ? settings.jqXHR.status : null; + if (json === null || (typeof status === 'number' && status == 204)) { + json = {}; + ajaxDataSrc(settings, json, []); + } + var error = json.error || json.sError; + if (error) { + log(settings, 0, error); + } + // Microsoft often wrap JSON as a string in another JSON object Let's + // handle that automatically + if (json.d && typeof json.d === 'string') { + try { + json = JSON.parse(json.d); + } + catch (e) { + // noop } } - if (firstTr) { - let colSizes = Dom.s(firstTr) - .children('th, td') - .mapTo(function (cell, idx) { - return { - idx: visibleToColumnIndex(settings, idx), - width: Dom.s(cell).width('outer') - }; - }); - // Check against what the colgroup > col is set to and correct if needed - for (let i = 0; i < colSizes.length; i++) { - let colEl = settings.columns[colSizes[i].idx].colEl; - colEl.css('width', colSizes[i].width + 'px'); - if (scroll.x) { - colEl.css('minWidth', colSizes[i].width + 'px'); + settings.json = json; + invalidColumn(settings); + callbackFire(settings, null, 'xhr', [settings, json, settings.jqXHR], true); + fn(json); + }; + if (util.is.plainObject(ajaxConfig) && ajaxConfig.data) { + ajaxData = ajaxConfig.data; + var newData = typeof ajaxData === 'function' + ? ajaxData(data, settings) // fn can manipulate data or return + : ajaxData; // an object or array to merge + // If the function returned something, use that alone + data = + typeof ajaxData === 'function' && newData + ? newData + : util.object.assignDeep(data, newData); + // Remove the data property as we've resolved it already and don't want + // jQuery to do it again (it is restored at the end of the function) + delete ajaxConfig.data; + } + var baseAjax = { + url: typeof ajaxConfig === 'string' ? ajaxConfig : '', + data: data, + success: callback, + dataType: 'json', + cache: false, + type: settings.serverMethod, + error: function (xhr, error) { + var ret = callbackFire(settings, null, 'xhr', [settings, null, settings.jqXHR], true); + if (ret.indexOf(false) === -1) { + if (error == 'parsererror') { + log(settings, 0, 'Invalid JSON response', 1); + } + else if (xhr.readyState === 4) { + log(settings, 0, 'Ajax error', 7); } } + processingDisplay(settings, false); } + }; + // If `ajax` option is an object, extend and override our default base + if (util.is.plainObject(ajaxConfig)) { + util.object.assign(baseAjax, ajaxConfig); } - // 3. Copy the colgroup over to the header and footer - divHeaderTable.find('colgroup').remove(); - divHeaderTable.append(settings.colgroup.clone(true)); - if (footer) { - divFooterTable.find('colgroup').remove(); - divFooterTable.append(settings.colgroup.clone(true)); - } - // "Hide" the header and footer that we used for the sizing. We need to keep - // the content of the cell so that the width applied to the header and body - // both match, but we want to hide it completely. - headerCopy.find('th, td').each(function (el) { - Dom.c('div') - .classAdd('dt-scroll-sizing') - .append(Array.from(el.childNodes)) - .appendTo(el); - }); - if (footerCopy) { - footerCopy.find('th, td').each(function (el) { - Dom.c('div') - .classAdd('dt-scroll-sizing') - .append(Array.from(el.childNodes)) - .appendTo(el); - }); - } - // 4. Clean up - // Figure out if there are scrollbar present - if so then we need the header and footer to - // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) - let isScrolling = Math.floor(table.height()) > divBodyEl.get(0).clientHeight || - divBody.css('overflow-y') == 'scroll'; - let paddingSide = 'padding' + (browser.scrollbarLeft ? 'Left' : 'Right'); - // Set the width's of the header and footer tables - let outerWidth = table.width('withPadding'); - divHeaderTable.css('width', stringToCss(outerWidth)); - divHeaderInner - .css('width', stringToCss(outerWidth)) - .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); - if (footer.count()) { - divFooterTable.css('width', stringToCss(outerWidth)); - divFooterInner - .css('width', stringToCss(outerWidth)) - .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); + // Store the data submitted for the API + settings.ajaxData = data; + // Allow plug-ins and external processes to modify the data + callbackFire(settings, null, 'preXhr', [settings, data, baseAjax], true); + if (typeof ajaxConfig === 'function') { + // Is a function - let the caller define what needs to be done + settings.jqXHR = ajaxConfig.call(instance, data, callback, settings); } - // Correct DOM ordering for colgroup - comes before the thead - table.children('colgroup').prependTo(table); - // Remove tabindex from the hidden row elements - table.find('thead, tfoot').find('[tabindex]').attrRemove('tabindex'); - // Dynamic ARIA roles - see setup for details on why this is needed - table - .find('thead, tfoot') - .attr('role', 'none') - .find('[role]') - .attrRemove('role'); - table.find('tbody tr:not([role])').attr('role', 'row'); - table.find('tbody td:not([role]), tbody th:not([role])').attr('role', 'cell'); - scrollAria(headerCopy); - scrollAria(footerCopy); - // Adjust the position of the header in case we loose the y-scrollbar - divBody.trigger('scroll'); - // If sorting or filtering has occurred, jump the scrolling back to the top - // only if we aren't holding the position - if ((settings.wasOrdered || settings.wasFiltered) && !settings.drawHold) { - divBodyEl.scrollTop(0); + else if (ajaxConfig && + typeof ajaxConfig !== 'string' && + ajaxConfig.url === '') { + // No url, so don't load any data. Just apply an empty data array + // to the object for the callback. + var empty = {}; + ajaxDataSrc(settings, empty, []); + callback(empty); } -} -/** - * Apply ARIA roles for the header / footer of a scrolling table - * @param element - */ -function scrollAria(element) { - if (element) { - element.find('tfoot:not([role])').attr('role', 'rowgroup'); - element.find('tr:not([role])').attr('role', 'row'); - element.find('th:not([role])').attr('role', 'columnheader'); - element.find('td:not([role])').attr('role', 'cell'); + else { + // Object to extend the base settings + settings.jqXHR = util.ajax(baseAjax); + } + // Restore for next time around + if (ajaxData) { + ajaxConfig.data = ajaxData; } } - /** - * Add a column to the list used for the table with default values + * Update the table using an Ajax call * * @param settings DataTables settings object + * @returns Block the table drawing or not */ -function addColumn(settings) { - // Add column to aoColumns array - let columnIdx = settings.columns.length; - let column = util.object.assign({}, new Settings(), defaults$4, { - orderData: defaults$4.orderData - ? defaults$4.orderData - : [columnIdx], - data: defaults$4.data ? defaults$4.data : columnIdx, - idx: columnIdx, - searchFixed: {}, - colEl: Dom - .c('col') - .attr('data-dt-column', columnIdx) +function ajaxUpdate(settings) { + settings.drawCount++; + processingDisplay(settings, true); + buildAjax(settings, ajaxParameters(settings), function (json) { + ajaxUpdateDraw(settings, json); }); - settings.columns.push(column); - // Legacy support for `searchCols` property. If set, and there is a value - // for this column, then it should be applied to the search. The new, column - // specific `search` option is applied in `columnOptions`, but we always - // want the search object for the column to exist. - let searchCols = settings.searchCols; - settings.searches[columnIdx] = create$2(searchCols[columnIdx] - ? hungarianToCamel(searchCols[columnIdx]) - : {}); - settings.searches[columnIdx].columns = [columnIdx]; +} +function functionOrValue(val) { + return typeof val === 'function' ? 'function' : val.toString(); } /** - * Apply options for a column + * Build up the parameters in an object needed for a server-side processing + * request. * * @param settings DataTables settings object - * @param colIdx column index to consider - * @param options Column configuration options + * @returns Block the table drawing or not */ -function columnOptions(settings, colIdx, options) { - var column = settings.columns[colIdx]; - /* User specified column options */ - if (options !== undefined && options !== null) { - // Backwards compatibility - compatCols(options); - if (options.type) { - column.typeManual = options.type; - } - // `class` is a reserved word in JavaScript, so we need to provide - // the ability to use a valid name for the camel case input - if (options.className && !options.className) { - options.className = options.className; - } - var origClass = column.className; - util.object.assign(column, options); - map(column, options, 'width', 'widthOrig'); - // Merge class from previously defined classes with this one, rather - // than just overwriting it in the extend above - if (origClass !== column.className) { - column.className = origClass + ' ' + column.className; +function ajaxParameters(settings) { + var columns = settings.columns, features = settings.features, searches = settings.searches, searchesFixed = settings.searchesFixed, colData = function (idx, prop) { + return typeof columns[idx][prop] === 'function' + ? 'function' + : columns[idx][prop]; + }; + return { + draw: settings.drawCount, + columns: columns.map(function (column, i) { + return { + data: colData(i, 'data'), + name: column.name, + searchable: column.searchable, + orderable: column.orderable, + search: { + value: searches[i] + ? functionOrValue(searches[i].search) + : '', + regex: searches[i] ? searches[i].regex : false, + fixed: searchesFixed[i] + ? Object.keys(searchesFixed[i]).map(name => ({ + name: name, + term: functionOrValue(searchesFixed[i][name].search) + })) + : [] + } + }; + }), + order: sortFlatten(settings).map(function (val) { + return { + column: val.col, + dir: val.dir, + name: colData(val.col, 'name') + }; + }), + start: settings.displayStart, + length: features.paging ? settings.pageLength : -1, + search: { + value: functionOrValue(searches['*'].search), + regex: searches['*'].regex, + fixed: Object.keys(settings.searchesFixed['*']).map(name => ({ + name: name, + term: functionOrValue(settings.searchesFixed['*'][name].search) + })), + groups: Object.keys(settings.searches) + .filter(c => c.includes(',')) // Limit to only multi-column subsets + .map(c => ({ + columns: settings.searches[c].columns || [], + term: functionOrValue(settings.searches[c].search) + })), + groupsFixed: Object.keys(settings.searchesFixed) + .filter(c => c.includes(',')) // Limit to only multi-column subsets + .map(c => { + let searches = settings.searchesFixed[c]; + return Object.keys(searches).map(n => ({ + columns: searches[n].columns || [], + name: n, + term: functionOrValue(searches[n].search) + })); + }) + .flat() } - map(column, options, 'orderData'); - // Search term specifically for this column - if (options.search) { - util.object.assign(settings.searches[colIdx], options.search); + }; +} +/** + * Data the data from the server (nuking the old) and redraw the table + * + * @param settings DataTables settings object + * @param json json data return from the server. + */ +function ajaxUpdateDraw(settings, json) { + var data = ajaxDataSrc(settings, json, false); + var drawUnique = ajaxDataSrcParam(settings, 'draw', json); + var recordsTotal = ajaxDataSrcParam(settings, 'recordsTotal', json); + var recordsFiltered = ajaxDataSrcParam(settings, 'recordsFiltered', json); + var existingTypes = settings.columns.map(c => c.type).join(','); + if (drawUnique !== undefined) { + // Protect against out of sequence returns + if (drawUnique * 1 < settings.drawCount) { + return; } + settings.drawCount = drawUnique * 1; } - /* Cache the data get and set functions for speed */ - var dataSrc = column.data; - var dataFn = util.get(dataSrc); - // The `render` option can be given as an array to access the helper - // rendering methods. The first element is the rendering method to use, the - // rest are the parameters to pass - if (column.render && Array.isArray(column.render)) { - var copy = column.render.slice(); - var name = copy.shift(); - column.render = helpers[name].apply(window, copy); + // No data in returned object, so rather than an array, we show an empty + // table + if (!data) { + data = []; } - column.renderer = column.render ? util.get(column.render) : null; - var attrTest = function (src) { - return typeof src === 'string' && src.indexOf('@') !== -1; - }; - column.attrSrc = - !!dataSrc && - util.is.plainObject(dataSrc) && - (attrTest(dataSrc.sort) || - attrTest(dataSrc.type) || - attrTest(dataSrc.filter)); - column.setter = null; - column.dataGet = function (rowData, type, meta) { - var innerData = dataFn(rowData, type, undefined, meta); - return column.renderer && type - ? column.renderer(innerData, type, rowData, meta) - : innerData; - }; - column.dataSet = function (rowData, val, meta) { - return util.set(dataSrc)(rowData, val, meta); - }; - // Indicate if DataTables should read DOM data as an object or array - // Used in _fnGetRowElements - if (typeof dataSrc !== 'number' && !column._isArrayHost) { - settings.rowReadObject = true; + clearTable(settings); + settings.recordsTotal = parseInt(recordsTotal, 10); + settings.recordsDisplay = parseInt(recordsFiltered, 10); + for (var i = 0, iLen = data.length; i < iLen; i++) { + addData(settings, data[i]); + } + settings.display = settings.displayMaster.slice(); + columnTypes(settings, existingTypes); + draw(settings, true); + initComplete(settings); + processingDisplay(settings, false); +} +/** + * Get the data from the JSON data source to use for drawing a table. + * + * @param settings DataTables settings object + * @param json Data source object / array from the server + * @param write Array or object to write the data to + * @return Array of data to use + */ +function ajaxDataSrc(settings, json, write) { + var dataProp = 'data'; + if (util.is.plainObject(settings.ajax) && + settings.ajax.dataSrc !== undefined) { + // Could in inside a `dataSrc` object, or not! + var dataSrc = settings.ajax.dataSrc; + // string, function and object are valid types + if (typeof dataSrc === 'string' || typeof dataSrc === 'function') { + dataProp = dataSrc; + } + else if (dataSrc.data !== undefined) { + dataProp = dataSrc.data; + } } - // Feature sorting overrides column specific when off - if (!settings.features.ordering) { - column.orderable = false; + if (!write) { + if (dataProp === 'data') { + // If the default, then we still want to support the old style, and + // safely ignore it if possible + return json.aaData || json[dataProp]; + } + return dataProp !== '' ? util.get(dataProp)(json) : json; } + // set + util.set(dataProp)(json, write); } /** - * Adjust the table column widths for new data. Note: you would probably want to - * do a redraw after calling this function! + * Very similar to ajaxDataSrc, but for the other SSP properties * * @param settings DataTables settings object + * @param param Target parameter + * @param json JSON data + * @returns Resolved value */ -function adjustColumnSizing(settings) { - calculateColumnWidths(settings); - columnSizes(settings); - let scroll = settings.scroll; - if (scroll.y !== '' || scroll.x !== '') { - scrollDraw(settings); +function ajaxDataSrcParam(settings, param, json) { + var dataSrc = util.is.plainObject(settings.ajax) + ? settings.ajax.dataSrc // TODO + : null; + if (dataSrc && dataSrc[param]) { + // Get from custom location + return util.data.get(dataSrc[param])(json); } - callbackFire(settings, null, 'column-sizing', [settings]); + // else - Default behaviour + var old = ''; + // Legacy support + if (param === 'draw') { + old = 'sEcho'; + } + else if (param === 'recordsTotal') { + old = 'iTotalRecords'; + } + else if (param === 'recordsFiltered') { + old = 'iTotalDisplayRecords'; + } + return json[old] !== undefined ? json[old] : json[param]; } + +const __filter_div = Dom.c('div').get(0); +const __filter_div_textContent = __filter_div.textContent !== undefined; /** - * Apply column sizes + * Filter the table using both the global filter and column based filtering * * @param settings DataTables settings object */ -function columnSizes(settings) { - let cols = settings.columns; - for (let i = 0; i < cols.length; i++) { - let width = columnsSumWidth(settings, [i], false); - if (width) { - cols[i].colEl.css('width', width); - if (settings.scroll.x) { - cols[i].colEl.css('min-width', width); - } - } +function filterComplete(settings) { + settings.columns; + // In server-side processing all filtering is done by the server, so no + // point hanging around here + if (dataSource(settings) != 'ssp') { + // Check if any of the rows were invalidated + filterData(settings); + // Start from the full data set + settings.display = settings.displayMaster.slice(); + // Column set filters first + util.object.each(settings.searches, (key, s) => { + filter(settings.display, settings, s.search, s); + }); + // Fixed (named) filters next + util.object.each(settings.searchesFixed, function (columns) { + util.object.each(settings.searchesFixed[columns], function (name, s) { + filter(settings.display, settings, s.search, s); + }); + }); + // And finally legacy global filtering + filterCustom(settings); } + // Tell the draw function we have been filtering + settings.wasFiltered = true; + callbackFire(settings, null, 'search', [settings]); } /** - * Convert the index of a visible column to the index in the data array (take - * account of hidden columns) + * Apply custom filtering functions + * + * This is legacy now that we have named functions, but it is widely used + * from 1.x, so it is not yet deprecated. * * @param settings DataTables settings object - * @param visIdx Visible column index to lookup - * @returns i the data index */ -function visibleToColumnIndex(settings, visIdx) { - let aiVis = getColumns(settings, 'visible'); - return typeof aiVis[visIdx] === 'number' ? aiVis[visIdx] : null; +function filterCustom(settings) { + let filters = ext.search; + let displayRows = settings.display; + let row, rowIdx; + for (let i = 0, iLen = filters.length; i < iLen; i++) { + let rows = []; + // Loop over each row and see if it should be included + for (let j = 0, jen = displayRows.length; j < jen; j++) { + rowIdx = displayRows[j]; + row = settings.data[rowIdx]; + if (row && + filters[i](settings, row.searchCellCache, rowIdx, row.data, j)) { + rows.push(rowIdx); + } + } + // So the array reference doesn't break set the results into the + // existing array + displayRows.length = 0; + arrayApply(displayRows, rows); + } } /** - * Convert the index of an index in the data array and convert it to the visible - * column index (take account of hidden columns) + * Filter the data table based on user input and draw the table * - * @param settings DataTables settings object - * @param match Column index to lookup - * @returns The data index + * @param searchRows + * @param settings + * @param input + * @param options + * @returns */ -function columnIndexToVisible(settings, match) { - let aiVis = getColumns(settings, 'visible'); - let iPos = aiVis.indexOf(match); - return iPos !== -1 ? iPos : null; +function filter(searchRows, settings, input, options) { + if (input === '') { + return; + } + let i = 0; + let matched = []; + // Search term can be a function, regex or string - if a string we apply our + // smart filtering regex (assuming the options require that) + let searchFunc = typeof input === 'function' ? input : null; + let rpSearch = input instanceof RegExp + ? input + : searchFunc + ? null + : filterCreateSearch(input, options); + let columns = options.columns + ? options.columns + : util.array.range(settings.columns.length); + // Then for each row, does the test pass. If not, lop the row from the array + for (i = 0; i < searchRows.length; i++) { + let row = settings.data[searchRows[i]]; + if (row) { + // Get the data array based on the columns to include in the search + let data = util.array.selectiveJoin(row.searchCellCache, columns); + // Run the search action + if ((searchFunc && + searchFunc(data, row.data, searchRows[i], columns.length === 1 ? columns[0] : columns // compat + )) || + (rpSearch && typeof data === 'string' && rpSearch.test(data))) { + matched.push(searchRows[i]); + } + } + } + // Mutate the searchRows array + searchRows.length = matched.length; + for (i = 0; i < matched.length; i++) { + searchRows[i] = matched[i]; + } } /** - * Get the number of visible columns - * - * @param settings DataTables settings object - * @returns i the number of visible columns + * Build a regular expression object suitable for searching a table */ -function visibleColumns(settings) { - let layout = settings.header; +function filterCreateSearch(searchIn, inOpts) { + let not = []; + let options = Object.assign({}, { + boundary: false, + caseInsensitive: true, + exact: false, + regex: false, + smart: true + }, inOpts); + let search = typeof searchIn !== 'string' ? searchIn.toString() : searchIn; + // Remove diacritics if normalize is set up to do so + search = util.diacritics(search); + if (options.exact) { + return new RegExp('^' + util.escapeRegex(search) + '$', options.caseInsensitive ? 'i' : ''); + } + search = options.regex ? search : util.escapeRegex(search); + if (options.smart) { + /* For smart filtering we want to allow the search to work regardless of + * word order. We also want double quoted text to be preserved, so word + * order is important - a la google. And a negative look around for + * finding rows which don't contain a given string. + * + * So this is the sort of thing we want to generate: + * + * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$ + */ + let parts = search.match(/!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g) || [ + '' + ]; + let a = parts.map(function (word) { + let negative = false; + let m; + // Determine if it is a "does not include" + if (word.charAt(0) === '!') { + negative = true; + word = word.substring(1); + } + // Strip the quotes from around matched phrases + if (word.charAt(0) === '"') { + m = word.match(/^"(.*)"$/); + word = m ? m[1] : word; + } + else if (word.charAt(0) === '\u201C') { + // Smart quote match (iPhone users) + m = word.match(/^\u201C(.*)\u201D$/); + word = m ? m[1] : word; + } + // For our "not" case, we need to modify the string that is + // allowed to match at the end of the expression. + if (negative) { + if (word.length > 1) { + not.push('(?!' + word + ')'); + } + word = ''; + } + return word.replace(/"/g, ''); + }); + let match = not.length ? not.join('') : ''; + let boundary = options.boundary ? '\\b' : ''; + search = + '^(?=.*?' + + boundary + + a.join(')(?=.*?' + boundary) + + ')(' + + match + + '.)*$'; + } + return new RegExp(search, options.caseInsensitive ? 'i' : ''); +} +// Update the filtering data for each row if needed (by invalidation or first +// run) +function filterData(settings) { let columns = settings.columns; - let vis = 0; - if (layout.length) { - for (let i = 0, iLen = layout[0].length; i < iLen; i++) { - if (columns[i].visible && - Dom.s(layout[0][i].cell).css('display') !== 'none') { - vis++; + let data = settings.data; + let column; + let j, jen, cellData, row; + let wasInvalidated = false; + for (let rowIdx = 0; rowIdx < data.length; rowIdx++) { + if (!data[rowIdx]) { + continue; + } + row = data[rowIdx]; + if (row && !row.searchCellCache) { + const rowFilterData = []; + for (j = 0, jen = columns.length; j < jen; j++) { + column = columns[j]; + if (column.searchable) { + cellData = getCellData(settings, rowIdx, j, 'filter'); + // Search in DataTables is string based + if (cellData === null) { + cellData = ''; + } + if (typeof cellData !== 'string' && cellData.toString) { + cellData = cellData.toString(); + } + } + else { + cellData = ''; + } + // If it looks like there is an HTML entity in the string, + // attempt to decode it so sorting works as expected. Note that + // we could use a single line of jQuery to do this, but the DOM + // method used here is much faster + // https://jsperf.com/html-decode + if (cellData.indexOf && cellData.indexOf('&') !== -1) { + __filter_div.innerHTML = cellData; + cellData = __filter_div_textContent + ? __filter_div.textContent + : __filter_div.innerText; + } + if (cellData.replace) { + cellData = cellData.replace(/[\r\n\u2028]/g, ''); + } + rowFilterData.push(cellData); } + row.searchCellCache = rowFilterData; + row.searchRowCache = rowFilterData.join(' '); + wasInvalidated = true; } } - return vis; + return wasInvalidated; } + /** - * Get an array of column indexes that match a given property + * Render and cache a row's display data for the columns, if required * * @param settings DataTables settings object - * @param param Parameter in the columns array to look for - * @returns Array of indexes with matched properties + * @param rowIdx Row index + * @returns Array with display information */ -function getColumns(settings, param) { - let a = []; - settings.columns.map(function (val, i) { - if (val[param]) { - a.push(i); +function getRowDisplay(settings, rowIdx) { + var rowModal = settings.data[rowIdx]; + var columns = settings.columns; + if (!rowModal) { + return []; + } + if (!rowModal.displayData) { + // Need to render and cache + rowModal.displayData = []; + for (var colIdx = 0, len = columns.length; colIdx < len; colIdx++) { + rowModal.displayData.push(getCellData(settings, rowIdx, colIdx, 'display')); } - }); - return a; + } + return rowModal.displayData; } /** - * Allow the result from a type detection function to be `true` while - * translating that into a string. Old type detection functions will return the - * type name if it passes. An object store would be better, but not backwards - * compatible. + * Create a new TR element (and it's TD children) for a row * - * @param typeDetect Object or function for type detection - * @param res Result from the type detection function - * @returns Type name or false - */ -function _typeResult(typeDetect, res) { - return res === true ? typeDetect._name : res; -} -/** - * Calculate the 'type' of a column * @param settings DataTables settings object + * @param rowIdx Row to consider + * @param trIn TR element to add to the table - optional. If not given, + * DataTables will create a row automatically + * @param tds Array of TD|TH elements for the row - must be given if trIn is. */ -function columnTypes(settings, originalTypes = '') { - var columns = settings.columns; - var data = settings.data; - var types = ext.type.detect; - var i, iLen, j, jen, k, ken; - var col, detectedType, cache; - if (!originalTypes) { - originalTypes = columns.map(c => c.type).join(','); - } - // For each column, spin over the data type detection functions, seeing if - // one matches - for (i = 0, iLen = columns.length; i < iLen; i++) { - col = columns[i]; - cache = []; - if (!col.type && col.typeManual) { - col.type = col.typeManual; - } - else if (!col.type) { - // With SSP type detection can be unreliable and error prone, so we - // provide a way to turn it off. - if (!settings.typeDetect) { - return; +function createTr(settings, rowIdx, trIn, tds) { + var row = settings.data[rowIdx], cells = [], tr, td, column, i, iLen, create, trClass = settings.classes.tbody.row; + if (row && row.tr === null) { + let rowData = row.data; + tr = trIn || document.createElement('tr'); + row.tr = tr; + row.cells = cells; + Dom.s(tr).classAdd(trClass); + // Use a private property on the node to allow reserve mapping from the node + // to the aoData array for fast look up + tr._DT_RowIndex = rowIdx; + // Special parameters can be given by the data source to be used on the + // row + rowAttributes(settings, row); + /* Process each column */ + for (i = 0, iLen = settings.columns.length; i < iLen; i++) { + column = settings.columns[i]; + create = trIn && tds && tds[i] ? false : true; + td = create + ? document.createElement(column.cellType) + : tds[i]; + if (!td) { + log(settings, 0, 'Incorrect column count', 18); } - for (j = 0, jen = types.length; j < jen; j++) { - let typeDetect = types[j]; - let oneOf; - let allOf; - let init; - let one = false; - // There can be either one, or three type detection functions - if (typeof typeDetect === 'function') { - allOf = typeDetect; - } - else { - oneOf = typeDetect.oneOf; - allOf = typeDetect.allOf; - init = typeDetect.init; - } - detectedType = null; - // Fast detect based on column assignment - if (init) { - detectedType = _typeResult(typeDetect, init(settings, col, i)); - if (detectedType) { - col.type = detectedType; - break; - } - } - for (k = 0, ken = data.length; k < ken; k++) { - if (!data[k]) { - continue; - } - // Use a cache array so we only need to get the type data - // from the formatter once (when using multiple detectors) - if (cache[k] === undefined) { - cache[k] = getCellData(settings, k, i, 'type'); - } - // Only one data point in the column needs to match this - // function - if (oneOf && !one) { - one = _typeResult(typeDetect, oneOf(cache[k], settings)); - } - // All data points need to match this function - detectedType = _typeResult(typeDetect, allOf(cache[k], settings)); - // If null, then this type can't apply to this column, so - // rather than testing all cells, break out. There is an - // exception for the last type which is `html`. We need to - // scan all rows since it is possible to mix string and HTML - // types - if (!detectedType && j !== types.length - 3) { - break; - } - // Only a single match is needed for html type since it is - // bottom of the pile and very similar to string - but it - // must not be empty - if (detectedType === 'html' && !util.is.empty(cache[k])) { - break; - } - } - // Type is valid for all data points in the column - use this - // type - if ((oneOf && one && detectedType) || - (!oneOf && detectedType)) { - col.type = detectedType; - break; - } + td._DT_CellIndex = { + row: rowIdx, + column: i + }; + cells.push(td); + var display = getRowDisplay(settings, rowIdx); + // Need to create the HTML if new, or if a rendering function is + // defined + if (create || + ((column.render || column.data !== i) && + (!util.is.plainObject(column.data) || + (column.data && + column.data._ !== i + '.display')))) { + writeCell(td, display[i]); } - // Fall back - if no type was detected, always use string - if (!col.type) { - col.type = 'string'; + // column class + Dom.s(td).classAdd(column.className); + // Visibility - add or remove as required + if (column.visible && create) { + tr.appendChild(td); + } + else if (!column.visible && !create) { + td.parentNode.removeChild(td); + } + if (column.createdCell) { + column.createdCell.call(settings.instance, td, getCellData(settings, rowIdx, i), rowData, rowIdx, i); } } - // Set class names for header / footer for auto type classes - var autoClass = ext.type.className[col.type]; - if (autoClass) { - _columnAutoClass(settings.header, i, autoClass); - _columnAutoClass(settings.footer, i, autoClass); - } - var renderer = ext.type.render[col.type]; - // This can only happen once! There is no way to remove - // a renderer. After the first time the renderer has - // already been set so createTr will run the renderer itself. - if (renderer && !col.renderer) { - col.renderer = util.get(renderer); - _columnAutoRender(settings, i); - } - } - var newTypes = columns.map(c => c.type).join(','); - if (newTypes !== originalTypes) { - callbackFire(settings, null, 'columnTypes', [settings], false); - } -} -/** - * Apply an auto detected renderer to data which doesn't yet have a renderer - */ -function _columnAutoRender(settings, colIdx) { - let data = settings.data; - for (let i = 0; i < data.length; i++) { - let d = data[i]; - if (d && d.tr) { - // We have to update the display here since there is no invalidation - // check for the data - let display = getCellData(settings, i, colIdx, 'display'); - d.displayData[colIdx] = display; - writeCell(d.cells[colIdx], display); - // No need to update sort / filter data since it has been - // invalidated and will be re-read with the renderer now applied - } + callbackFire(settings, 'rowCreated', 'row-created', [ + tr, + rowData, + rowIdx, + cells + ]); + } + else if (row) { + Dom.s(row.tr).classAdd(trClass); } } /** - * Apply a class name to a column's header cells + * Add attributes to a row based on the special `DT_*` parameters in a data + * source object. * - * @param container The header / footer structure array - * @param colIdx Column index - * @param className Class name to apply + * @param settings DataTables settings object + * @param row Row object for the row to be modified */ -function _columnAutoClass(container, colIdx, className) { - container.forEach(function (row) { - if (row[colIdx] && row[colIdx].unique) { - Dom.s(row[colIdx].cell).classAdd(className); +function rowAttributes(settings, row) { + var tr = row.tr; + var data = row.data; + if (tr) { + var id = settings.rowIdFn(data); + if (id) { + tr.id = id; } - }); + if (data.DT_RowClass) { + // Remove any classes added by DT_RowClass before + var a = data.DT_RowClass.split(' '); + row.addedClasses = row.addedClasses + ? util.unique(row.addedClasses.concat(a)) + : a; + Dom.s(tr) + .classRemove(row.addedClasses.join(' ')) + .classAdd(data.DT_RowClass); + } + if (data.DT_RowAttr) { + Dom.s(tr).attr(data.DT_RowAttr); + } + if (data.DT_RowData) { + Dom.s(tr).data(data.DT_RowData); + } + } } /** - * Take the column definitions and static columns arrays and calculate how they - * relate to column indexes. The callback function will then apply the - * definition found for a column to a suitable configuration object. + * Create the HTML header for the table * - * @param settings DataTables settings object - * @param aoColDefs The aoColumnDefs array that is to be applied - * @param aoCols The aoColumns array that defines columns individually - * @param headerLayout Layout for header as it was loaded - * @param fn Callback function - takes two parameters, the calculated column - * index and the definition for that column. + * @param settings DataTable instance + * @param side If the header or footer should be used + * @returns */ -function applyColumnDefs(settings, aoColDefs, aoCols, headerLayout, fn) { - var i, iLen, j, jLen, k, kLen; - var columns = settings.columns; - if (aoCols) { - for (i = 0, iLen = aoCols.length; i < iLen; i++) { - // Compat - if (aoCols[i] && aoCols[i].name) { - columns[i].name = aoCols[i].name; - } - } +function buildHead(settings, side) { + let classes = settings.classes; + let columns = settings.columns; + let i, iLen, row; + let target = Dom.s(side === 'header' ? settings.thead : settings.tfoot); + let titleProp = side === 'header' ? 'title' : side; + // Footer might be defined + if (!target) { + return; } - // Column definitions with aTargets - if (aoColDefs) { - // Loop over the definitions array - loop in reverse so first instance - // has priority - for (i = aoColDefs.length - 1; i >= 0; i--) { - let def = aoColDefs[i]; - /* Each definition can target multiple columns, as it is an array */ - let aTargets = def.target !== undefined - ? def.target - : def.targets !== undefined - ? def.targets - : def.aTargets; // legacy - if (!Array.isArray(aTargets)) { - aTargets = [aTargets]; - } - for (j = 0, jLen = aTargets.length; j < jLen; j++) { - var target = aTargets[j]; - if (typeof target === 'number' && target >= 0) { - /* Add columns that we don't yet know about */ - while (columns.length <= target) { - addColumn(settings); - } - /* Integer, basic index */ - fn(target, def); - } - else if (typeof target === 'number' && target < 0) { - /* Negative integer, right to left column counting */ - fn(columns.length + target, def); - } - else if (typeof target === 'string') { - for (k = 0, kLen = columns.length; k < kLen; k++) { - if (target === '_all') { - // Apply to all columns - fn(k, def); - } - else if (target.indexOf(':name') !== -1) { - // Column selector - if (columns[k].name === target.replace(':name', '')) { - fn(k, def); - } - } - else { - // Cell selector - headerLayout.forEach(function (row) { - if (row[k]) { - var cell = row[k].cell; - // Legacy support. Note that it means that - // we don't support an element name selector - // only, since they are treated as class - // names for 1.x compat. - if (target.match(/^[a-z][\w-]*$/i)) { - target = '.' + target; - } - if (cell.matches(target)) { - fn(k, def); - } - } - }); - } - } - } + // If no cells yet and we have content for them, then create + if (side === 'header' || + util.array.pluck(settings.columns, titleProp).join('')) { + row = target.find('tr'); + // Add a row if needed + if (!row.count()) { + row = Dom.c('tr').appendTo(target); + } + // Add the number of cells needed to make up to the number of columns + if (row.count() === 1) { + let cellCount = 0; + row.find('td, th').each(el => { + cellCount += el.colSpan; + }); + for (i = cellCount, iLen = columns.length; i < iLen; i++) { + Dom.c('th') + .html(columns[i][titleProp] || '') + .appendTo(row); } } } - // Statically defined columns array - if (aoCols) { - for (i = 0, iLen = aoCols.length; i < iLen; i++) { - fn(i, aoCols[i]); - } + let detected = detectHeader(settings, target.get(0), true); + if (side === 'header') { + settings.header = detected; + target.find('tr').classAdd(classes.thead.row); + } + else { + settings.footer = detected; + target.find('tr').classAdd(classes.tfoot.row); } + // Every cell needs to be passed through the renderer + target + .children('tr') + .children('th, td') + .each(el => { + // Should just be able to do `renderer(settings, side)` here but + // Typescript doesn't like it, despite it already being constrained! + let runner = side === 'header' + ? renderer(settings, 'header') + : renderer(settings, 'footer'); + runner(settings, Dom.s(el), classes); + }); } /** - * Get the width for a given set of columns + * Build a layout structure for a header or footer * - * @param settings DataTables settings object - * @param targets Columns - comma separated string or array of numbers - * @param original Use the original width (true) or calculated (false) - * @param incVisible Include visible columns (true) or not (false) - * @returns Combined CSS value + * @param settings DataTables settings + * @param source Source layout array + * @param incColumns What columns should be included + * @returns Layout array in column index order */ -function columnsSumWidth(settings, targets, original, incVisible) { - if (!Array.isArray(targets)) { - targets = columnsFromHeader(targets); +function headerLayout(settings, source, incColumns) { + var row, column, cell; + var local = []; + var structure = []; + var columns = settings.columns; + var columnCount = columns.length; + var rowspan, colspan; + if (!source) { + return; } - let sum = 0; - let unit = 'px'; - let columns = settings.columns; - for (let i = 0, iLen = targets.length; i < iLen; i++) { - let column = columns[targets[i]]; - let definedWidth = original ? column.widthOrig : column.width; - if (column.visible === false) { - continue; - } - if (definedWidth === null || definedWidth === undefined) { - return null; // can't determine a defined width - browser defined - } - else if (typeof definedWidth === 'number') { - sum += definedWidth; - } - else { - let matched = definedWidth.match(/([\d\.]+)([^\d]*)/); - if (matched) { - sum += parseFloat(matched[1]); - unit = matched.length === 3 ? matched[2] : 'px'; + // Default is to work on only visible columns + if (!incColumns) { + incColumns = util.array.range(columnCount).filter(function (idx) { + return columns[idx].visible; + }); + } + // Make a copy of the master layout array, but with only the columns we want + for (row = 0; row < source.length; row++) { + // Remove any columns we haven't selected + local[row] = source[row].slice().filter(function (c, i) { + return incColumns.includes(i); + }); + // Prep the structure array - it needs an element for each row + structure.push([]); + } + for (row = 0; row < local.length; row++) { + for (column = 0; column < local[row].length; column++) { + rowspan = 1; + colspan = 1; + // Check to see if there is already a cell (row/colspan) covering + // our target insert point. If there is, then there is nothing to + // do. + if (structure[row][column] === undefined) { + cell = local[row][column].cell; + // Expand for rowspan + while (local[row + rowspan] !== undefined && + local[row][column].cell == local[row + rowspan][column].cell) { + structure[row + rowspan][column] = null; + rowspan++; + } + // And for colspan + while (local[row][column + colspan] !== undefined && + local[row][column].cell == local[row][column + colspan].cell) { + // Which also needs to go over rows + for (var k = 0; k < rowspan; k++) { + structure[row + k][column + colspan] = null; + } + colspan++; + } + var titleSpan = Dom.s(cell).find('.dt-column-title'); + structure[row][column] = { + cell: cell, + colspan: colspan, + rowspan: rowspan, + title: titleSpan.count() + ? titleSpan.html() + : Dom.s(cell).html() + }; } } } - return sum + unit; + return structure; } /** - * Determine what columns a header cell covers (can be multiple for colspan - * cases). + * Draw the header (or footer) element based on the column visibility states. * - * @param cell The header cell in question - * @returns An array of column indexes + * @param settings DataTables settings object + * @param source Layout array from detectHeader */ -function columnsFromHeader(cell) { - let attr = Dom.s(cell).closest('[data-dt-column]').attr('data-dt-column'); - if (!attr) { - return []; +function drawHead(settings, source) { + let layout = headerLayout(settings, source); + let tr; + if (!layout) { + return; } - return attr.split(',').map(function (val) { - return parseInt(val); - }); -} - -/** - * Generate the node required for the processing node - * - * @param ctx DataTables settings object - */ -function processingHtml(ctx) { - var table = ctx.table; - var scrolling = ctx.scroll.x !== '' || ctx.scroll.y !== ''; - if (ctx.features.processing) { - var n = Dom - .c('div') - .attr('id', ctx.tableId + '_processing') - .attr('role', 'status') - .classAdd(ctx.classes.processing.container) - .html(ctx.language.processing) - .append(Dom - .c('div') - .append(Dom.c('div')) - .append(Dom.c('div')) - .append(Dom.c('div')) - .append(Dom.c('div'))); - // Different positioning depending on if scrolling is enabled or not - if (scrolling) { - n.prependTo(Dom.s(ctx.tableWrapper).find('div.dt-scroll').get(0)); + for (let row = 0; row < source.length; row++) { + tr = source[row].row; + // All cells are going to be replaced, so empty out the row + if (tr) { + Dom.s(tr).detachChildren(); } - else { - n.insertBefore(table); + for (let column = 0; column < layout[row].length; column++) { + let point = layout[row][column]; + if (point) { + Dom.s(point.cell) + .appendTo(tr) + .attr('rowspan', point.rowspan) + .attr('colspan', point.colspan); + } } - Dom.s(table).on('processing.dt.DT', (e, s, show) => { - n.css('display', show ? 'block' : 'none'); - }); } } /** - * Display or hide the processing indicator + * Insert the required TR nodes into the table for display * - * @param ctx DataTables settings object - * @param show Show the processing indicator (true) or not (false) + * @param settings DataTables settings object + * @param ajaxComplete true after ajax call to complete rendering */ -function processingDisplay(ctx, show) { - // Ignore cases when we are still redrawing - if (ctx.doingDraw && show === false) { +function draw(settings, ajaxComplete) { + // Allow for state saving and a custom start position + setStartPosition(settings); + // Provide a pre-callback function which can be used to cancel the draw is + // false is returned + var aPreDraw = callbackFire(settings, 'preDraw', 'preDraw', [settings]); + if (aPreDraw.indexOf(false) !== -1) { + processingDisplay(settings, false); return; } - callbackFire(ctx, null, 'processing', [ctx, show]); -} -/** - * Show the processing element if an action takes longer than a given time - * - * @param ctx DataTables settings object - * @param enable Do (true) or not (false) async processing (local feature enablement) - * @param run Function to run - */ -function processingRun(ctx, enable, run) { - if (!enable) { - // Immediate execution, synchronous - run(); - } - else { - processingDisplay(ctx, true); - // Allow the processing display to show if needed - setTimeout(function () { - run(); - processingDisplay(ctx, false); - }, 0); + var rowEls = []; + var rowCount = 0; + var isServerSide = dataSource(settings) == 'ssp'; + var display = settings.display; + var start = settings.displayStart; + var end = displayEnd(settings); + var columns = settings.columns; + var body = Dom.s(settings.tbody); + settings.doingDraw = true; + /* Server-side processing draw intercept */ + if (settings.deferLoading) { + settings.deferLoading = false; + settings.drawCount++; + processingDisplay(settings, false); } -} - -function renderer(ctx, type) { - var render = ctx.renderer; - var host = ext.renderer[type]; - if (plainObject(render) && render[type]) { - // Specific renderer for this type. If available use it, otherwise use - // the default. - return host[render[type]] || host._; + else if (!isServerSide) { + settings.drawCount++; } - else if (typeof render === 'string') { - // Common renderer - if there is one available for this type use it, - // otherwise use the default - return host[render] || host._; + else if (!settings.destroying && !ajaxComplete) { + // Show loading message for server-side processing + if (settings.drawCount === 0) { + body.empty().append(_emptyRow(settings)); + } + ajaxUpdate(settings); + return; } - // Use the default - return host._; -} - -/** - * Add the options to the page HTML for the table - * - * @param ctx DataTables context - */ -function createLayout(ctx) { - var classes = ctx.classes; - // Wrapper div around everything DataTables controls - var insert = Dom - .c('div') - .attr('id', ctx.tableId + '_wrapper') - .classAdd(classes.container) - .insertBefore(ctx.table); - ctx.tableWrapper = insert.get(0); - if (ctx.dom) { - // Legacy - legacyDom(ctx, ctx.dom, insert); + if (display.length !== 0) { + var iStart = isServerSide ? 0 : start; + var iEnd = isServerSide ? settings.data.length : end; + for (var j = iStart; j < iEnd; j++) { + var dataIdx = display[j]; + var data = settings.data[dataIdx]; + // Row has been deleted - can't be displayed + if (data === null) { + continue; + } + // Row node hasn't been created yet + if (data.tr === null) { + createTr(settings, dataIdx); + } + var nRow = data.tr; + // Add various classes as needed + for (var i = 0; i < columns.length; i++) { + var col = columns[i]; + var td = data.cells[i]; + Dom.s(td) + .classAdd(col.type ? ext.type.className[col.type] : null) // auto class + .classAdd(settings.classes.tbody.cell); // all cells + } + // Row callback functions - might want to manipulate the row + // rowCount and j are not currently documented. Are they at all + // useful? + callbackFire(settings, 'row', null, [ + nRow, + data.data, + rowCount, + j, + dataIdx + ]); + rowEls.push(nRow); + rowCount++; + } } else { - var top = convert(ctx, ctx.layout, 'top'); - var bottom = convert(ctx, ctx.layout, 'bottom'); - var render = renderer(ctx, 'layout'); - // Everything above - the renderer will actually insert the contents into the document - top.forEach(function (item) { - render(ctx, insert, item); - }); - // The table - always the center of attention - render(ctx, insert, { - full: { - contents: [featureTable(ctx)], - items: [], - table: true - } - }); - // Everything below - bottom.forEach(function (item) { - render(ctx, insert, item); - }); + rowEls[0] = _emptyRow(settings); } - // Processing floats on top, so it isn't an inserted feature - processingHtml(ctx); + /* Header and footer callbacks */ + callbackFire(settings, 'header', 'header', [ + Dom.s(settings.thead).children('tr').get(0), + getDataMaster(settings), + start, + end, + display + ]); + callbackFire(settings, 'footer', 'footer', [ + Dom.s(settings.tfoot).children('tr').get(0), + getDataMaster(settings), + start, + end, + display + ]); + body.detachChildren().append(rowEls); + // Empty table needs a specific class + Dom.s(settings.tableWrapper).classToggle('dt-empty-footer', Dom.s(settings.tfoot).find('tr').count() === 0); + // Call all required callback functions for the end of a draw + callbackFire(settings, 'draw', 'draw', [settings], true); + // Draw is complete, sorting and filtering must be as well + settings.wasOrdered = false; + settings.wasFiltered = false; + settings.doingDraw = false; } /** - * Expand the layout items into an object for the rendering function + * Redraw the table - taking account of the various features which are enabled + * + * @param settings DataTables settings object + * @param holdPosition Keep the current paging position. By default the paging + * is reset to the first page + * @param recompute Indicate if a rebuild of sort and filter should happen */ -function layoutItems(row, align, items) { - if (Array.isArray(items)) { - for (var i = 0; i < items.length; i++) { - layoutItems(row, align, items[i]); +function reDraw(settings, holdPosition, recompute) { + let features = settings.features, doSort = features.ordering, doFilter = features.searching; + if (recompute === undefined || recompute === true) { + // Resolve any column types that are unknown due to addition or + // invalidation + columnTypes(settings); + columnWidths(settings); + if (doSort) { + sort(settings); } - return; - } - var rowCell = row[align]; // can't be undefined - will have been created by getRow - // If it is an object, then there can be multiple features contained in it - if (util.is.plainObject(items)) { - // Is it an cell object already, with rowId, etc. A feature plugin cannot - // be named "features" due to this check - if (items.features) { - if (items.rowId) { - row.id = items.rowId; - } - if (items.rowClass) { - row.className = items.rowClass; - } - rowCell.id = items.id; - rowCell.className = items.className; - layoutItems(row, align, items.features); + if (doFilter) { + filterComplete(settings); } else { - // An object of features and configuration options - e.g. `{paging: {startEnd: false}}` - util.object.each(items, (key, val) => { - rowCell.items.push({ - feature: key, - opts: val - }); - }); + // No filtering, so we want to just use the display master + settings.display = settings.displayMaster.slice(); } } + if (holdPosition !== true) { + settings.displayStart = 0; + } else { - // Otherwise, it is a function, node or Dom / jQuery instance and can just get added - rowCell.items.push(items); + // Keep position, but make sure that there is actually data to display, + // otherwise we need to rewind a bit (e.g. if rows were deleted) + lengthOverflow(settings); } + // Let any modules know about the draw hold position state (used by + // scrolling internally) + settings.drawHold = holdPosition; + draw(settings); + settings.api.one('draw', function () { + settings.drawHold = false; + }); } /** - * Find, or create a layout row and setup a target cell in it + * Table is empty - create a row with an empty message in it * - * @param rows Rows array to search for the target row. Is mutated when a row is - * added if not found. - * @param rowNum Row index to get - * @param align Where the cell position is - * @returns The row + * @param settings DataTables context */ -function getRow(rows, rowNum, align) { - var row; - // Find existing rows - for (var i = 0; i < rows.length; i++) { - row = rows[i]; - if (row.rowNum === rowNum) { - // full is on its own, but start and end share a row - if ((align === 'full' && row.full) || - ((align === 'start' || align === 'end') && - (row.start || row.end))) { - if (!row[align]) { - row[align] = { - contents: [], - items: [] - }; +function _emptyRow(settings) { + let lang = settings.language; + let zero = lang.zeroRecords; + let dataSrc = dataSource(settings); + // Make use of the fact that settings.json is only set once the initial data + // has been loaded. Show loading when that isn't the case + if ((dataSrc === 'ssp' || dataSrc === 'ajax') && !settings.json) { + zero = lang.loadingRecords; + } + else if (lang.emptyTable && recordsTotal(settings) === 0) { + zero = lang.emptyTable; + } + return Dom + .c('tr') + .append(Dom + .c('td') + .attr('colSpan', visibleColumns(settings)) + .classAdd(settings.classes.empty.row) + .html(zero)) + .get(0); +} +/** + * Use the DOM source to create up an array of header cells. The idea here is to + * create a layout grid (array) of rows x columns, which contains a reference to + * the cell at that point in the grid (regardless of col/rowspan), such that any + * column / row could be removed and the new grid constructed. + * + * @param settings DataTables context + * @param thead thead / tbody element + * @param write If cells should be written (if required) + * @returns Calculated layout array + */ +function detectHeader(settings, thead, write) { + let columns = settings.columns; + let rows = Dom.s(thead).children('tr'); + let row, loopCell; + let i, k, l, len, shifted, column, colspan, rowspan; + let titleRow = settings.titleRow; + let isHeader = thead && thead.nodeName.toLowerCase() === 'thead'; + let layout = []; + let isUnique; + let shift = function (a, b, j) { + let d = a[b]; + while (d[j]) { + j++; + } + return j; + }; + // We know how many rows there are in the layout - so prep it + for (i = 0, len = rows.count(); i < len; i++) { + layout.push([]); + } + for (i = 0, len = rows.count(); i < len; i++) { + row = rows.get(i); + column = 0; + // For every cell in the row.. + loopCell = row.firstChild; + while (loopCell) { + if (loopCell.nodeName.toUpperCase() == 'TD' || + loopCell.nodeName.toUpperCase() == 'TH') { + let cell = Dom.s(loopCell); + let cols = []; + // Get the col and rowspan attributes from the DOM and sanitise + // them + colspan = parseInt(cell.attr('colspan') || '1') || 1; + rowspan = parseInt(cell.attr('rowspan') || '1') || 1; + colspan = + !colspan || colspan === 0 || colspan === 1 ? 1 : colspan; + rowspan = + !rowspan || rowspan === 0 || rowspan === 1 ? 1 : rowspan; + // There might be colspan cells already in this row, so shift + // our target accordingly + shifted = shift(layout, i, column); + // Cache calculation for unique columns + isUnique = colspan === 1 ? true : false; + // Perform header setup + if (write) { + if (isUnique) { + // Allow column options to be set from HTML attributes + columnOptions(settings, shifted, escapeObject(cell.data())); + // Get the width for the column. This can be defined + // from the width attribute, style attribute or + // `columns.width` option + let columnDef = columns[shifted]; + let width = cell.attr('width') || null; + let t = cell + .get(0) + .style.width.match(/width:\s*(\d+[pxem%]+)/); + if (t) { + width = t[1]; + } + columnDef.widthOrig = columnDef.width || width; + if (isHeader) { + // Column title handling - can be user set, or read + // from the DOM This happens before the render, so + // the original is still in place + if (columnDef.title !== null && + !columnDef.autoTitle) { + if ((titleRow === true && i === 0) || // top row + (titleRow === false && + i === rows.count() - 1) || // bottom row + titleRow === i || // specific row + titleRow === null) { + cell.html(columnDef.title); + } + } + if (!columnDef.title && isUnique) { + columnDef.title = util.string.stripHtml(cell.html()); + columnDef.autoTitle = true; + } + } + else { + // Footer specific operations + if (columnDef.footer) { + cell.html(columnDef.footer); + } + } + // Fall back to the aria-label attribute on the table + // header if no ariaTitle is provided. + if (!columnDef.ariaTitle) { + columnDef.ariaTitle = + cell.attr('aria-label') || columnDef.title; + } + // Column specific class names + if (columnDef.className) { + cell.classAdd(columnDef.className); + } + } + // Wrap the column title so we can write to it in future + if (cell.find('div.dt-column-title').count() === 0) { + Dom.c('div') + .classAdd('dt-column-title') + .append(Array.from(cell.get(0).childNodes)) + .appendTo(cell); + } + if (settings.orderIndicators && + isHeader && + cell.filter(':not([data-dt-order=disable])').count() !== + 0 && + cell.parent(':not([data-dt-order=disable])').count() !== + 0 && + cell.find('div.dt-column-order').count() === 0) { + Dom.c('div') + .classAdd('dt-column-order') + .appendTo(cell); + } + // We need to wrap the elements in the header in another + // element to use flexbox layout for those elements + var headerFooter = isHeader ? 'header' : 'footer'; + if (cell.find('div.dt-column-' + headerFooter).count() === + 0) { + Dom.c('div') + .classAdd('dt-column-' + headerFooter) + .append(Array.from(cell.get(0).childNodes)) + .appendTo(cell); + } + } + // If there is col / rowspan, copy the information into the + // layout grid + for (l = 0; l < colspan; l++) { + for (k = 0; k < rowspan; k++) { + layout[i + k][shifted + l] = { + cell: cell.get(0), + unique: isUnique + }; + layout[i + k].row = row; + } + cols.push(shifted + l); } - return row; + // Assign an attribute so spanning cells can still be identified + // as belonging to a column + cell.attr('data-dt-column', util.unique(cols).join(',')); } + loopCell = loopCell.nextSibling; } } - // If we get this far, then there was no match, create a new row - row = { - rowNum: rowNum - }; - row[align] = { - contents: [], - items: [] - }; - rows.push(row); - return row; + return layout; } /** - * Convert a `layout` object given by a user to the object structure needed - * for the renderer. This is done twice, once for above and once for below - * the table. Ordering must also be considered. + * Set the start position for draw * * @param settings DataTables settings object - * @param layout Layout object to convert - * @param side `top` or `bottom` - * @returns Converted array structure - one item for each row. */ -function convert(settings, layout, side) { - var rows = []; - // Split out into an array - util.object.each(layout, function (pos, items) { - var parts = pos.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/); - if (items === null || !parts) { - return; - } - var rowNum = parts[2] ? parseInt(parts[2]) : 0; - var align = parts[3] ? parts[3].toLowerCase() : 'full'; - // Filter out the side we aren't interested in - if (parts[1] !== side) { - return; - } - // Only really a type check - if (align !== 'full' && align !== 'start' && align !== 'end') { - return; - } - // Get or create the row we should attach to - var row = getRow(rows, rowNum, align); - layoutItems(row, align, items); - }); - // Order by item identifier - rows.sort(function (a, b) { - var order1 = a.rowNum || 0; - var order2 = b.rowNum || 0; - // If both in the same row, then the row with `full` comes first - if (order1 === order2) { - var ret = a.full && !b.full ? -1 : 1; - return side === 'bottom' ? ret * -1 : ret; - } - return order2 - order1; - }); - // Invert for below the table - if (side === 'bottom') { - rows.reverse(); - } - for (var row = 0; row < rows.length; row++) { - delete rows[row].rowNum; - resolve(settings, rows[row]); +function setStartPosition(settings) { + var bServerSide = dataSource(settings) == 'ssp'; + var iInitDisplayStart = settings.displayStartInit; + // Check and see if we have an initial draw position from state saving + if (iInitDisplayStart !== undefined && iInitDisplayStart !== -1) { + settings.displayStart = bServerSide + ? iInitDisplayStart + : iInitDisplayStart >= recordsDisplay(settings) + ? 0 + : iInitDisplayStart; + settings.displayStartInit = -1; } - return rows; } /** - * Convert the contents of a row's layout object to nodes that can be inserted - * into the document by a renderer. Execute functions, look up plug-ins, etc. + * Get the number of records in the current record set, before filtering * - * @param settings DataTables settings object - * @param row Layout object for this row + * @param ctx DataTables settings object */ -function resolve(settings, row) { - var getFeature = function (feature, opts) { - if (!ext.features[feature]) { - log(settings, 0, 'Unknown feature: ' + feature); - } - return ext.features[feature].apply(this, [settings, opts]); - }; - // Resolve items in the `contents` array from being an identifier, such as - // the name of a feature, into the node to display. - var resolve = function (item) { - if (!row[item]) { - return; - } - row[item].contents = row[item].items - .filter(item => !!item) - .map(item => { - if (typeof item === 'string') { - return getFeature(item, null); - } - else if (util.is.plainObject(item)) { - // If it's an object, it just has feature and opts properties from - // the transform in _layoutArray - return getFeature(item.feature, item.opts); - } - else if (typeof item.node === 'function') { - return item.node(settings); - } - else if (typeof item === 'function') { - var inst = item(settings); - return typeof inst.node === 'function' ? inst.node() : inst; - } - else if (item.nodeName) { - // An HTML element - return item; - } - else if (item instanceof Dom) { - return item.get(0); - } - else if (item.length) { - // Possibly jQuery - return item[0]; - } - }); - }; - resolve('start'); - resolve('end'); - resolve('full'); +function recordsTotal(ctx) { + return dataSource(ctx) == 'ssp' + ? ctx.recordsTotal * 1 + : ctx.displayMaster.length; } /** - * Draw the table with the legacy DOM property + * Get the number of records in the current record set, after filtering * - * @param settings DT settings instance - * @param layout DOM string - * @param insert Insert point + * @param ctx DataTables settings object */ -function legacyDom(settings, layout, insert) { - let parts = layout.match(/(".*?")|('.*?')|./g); - let featureNode, option, newNode, next, attr; - if (!parts) { - return; +function recordsDisplay(ctx) { + return dataSource(ctx) == 'ssp' + ? ctx.recordsDisplay * 1 + : ctx.display.length; +} +/** + * Get the display end point - display index + * + * @param ctx DataTables settings object + */ +function displayEnd(ctx) { + var len = ctx.pageLength, start = ctx.displayStart, calc = start + len, records = ctx.display.length, features = ctx.features, paginate = features.paging; + if (features.serverSide) { + return paginate === false || len === -1 + ? start + records + : Math.min(start + len, ctx.recordsDisplay); } - for (let i = 0; i < parts.length; i++) { - featureNode = null; - option = parts[i]; - if (option == '<') { - // New container div - newNode = Dom.c('div'); - // Check to see if we should append an id and/or a class name to the container - next = parts[i + 1]; - if (next[0] == "'" || next[0] == '"') { - attr = next.replace(/['"]/g, ''); - let id = '', className; - /* The attribute can be in the format of "#id.class", "#id" or "class" This logic - * breaks the string into parts and applies them as needed - */ - if (attr.indexOf('.') != -1) { - let split = attr.split('.'); - id = split[0]; - className = split[1]; - } - else if (attr[0] == '#') { - id = attr; - } - else { - className = attr; - } - newNode.attr('id', id.substring(1)).classAdd(className); - i++; // Move along the position array - } - insert.append(newNode.get()); // TODO - insert = newNode; + else { + return !paginate || calc > records || len === -1 ? records : calc; + } +} + +/** + * Log an error message + * + * @param ctx DataTables settings object + * @param level log error messages, or display them to the user + * @param msg error message + * @param tn Technical note id to get more information about the error. + */ +function log(ctx, level, msg, tn) { + msg = + 'DataTables warning: ' + + (ctx ? 'table id=' + ctx.tableId + ' - ' : '') + + msg; + if (tn) { + msg += + '. For more information about this error, please see ' + + 'https://datatables.net/tn/' + + tn; + } + { + // Backwards compatibility pre 1.10 + var type = ext.sErrMode || ext.errMode; + if (ctx) { + callbackFire(ctx, null, 'dt-error', [ctx, tn, msg], true); } - else if (option == '>') { - // End container div - insert = insert.parent(); + if (type == 'alert') { + alert(msg); } - else if (option == 't') { - // Table - featureNode = featureTable(settings); + else if (type == 'throw') { + throw new Error(msg); } - else { - ext.feature.forEach(function (feature) { - if (option == feature.cFeature) { - featureNode = feature.fnInit(settings); - } - }); + else if (typeof type == 'function') { + type(ctx, tn, msg); } - // Add to the display - if (featureNode) { - // TODO when doing the full dom update, won't need this check - insert.append(featureNode instanceof Dom ? featureNode.get() : featureNode); + } +} +/** + * See if a property is defined on one object, if so assign it to the other + * object + * + * @param ret target object + * @param src source object + * @param name property + * @param mappedName name to map too - optional, name used if not given + */ +function map(ret, src, name, mappedName) { + if (Array.isArray(name)) { + for (let i = 0; i < name.length; i++) { + let val = name[i]; + if (Array.isArray(val)) { + map(ret, src, val[0], val[1]); + } + else { + map(ret, src, val); + } } + return; } -} - -function sortInit(settings) { - var target = settings.thead; - var headerRows = target.querySelectorAll('tr'); - var titleRow = settings.titleRow; - var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])'; - // Legacy support for `orderCellsTop` - if (titleRow === true) { - target = headerRows[0]; - } - else if (titleRow === false) { - target = headerRows[headerRows.length - 1]; - } - else if (titleRow !== null) { - target = headerRows[titleRow]; + if (mappedName === undefined) { + mappedName = name; } - // else - all rows - if (settings.orderHandler) { - sortAttachListener(settings, target, target === settings.thead - ? 'tr' + - notSelector + - ' th' + - notSelector + - ', tr' + - notSelector + - ' td' + - notSelector - : 'th' + notSelector + ', td' + notSelector); + if (src[name] !== undefined) { + ret[mappedName] = src[name]; } - // Need to resolve the user input array into our internal structure - var order = []; - sortResolve(settings, order, settings.order); - settings.order = order; } /** - * Attach event listeners to a node that will trigger ordering on a column + * Bind an event handler to allow a click or return key to activate the callback. + * This is good for accessibility since a return on the keyboard will have the + * same effect as a click, if the element has focus. * - * @param settings DataTables context - * @param node Node to attach to - * @param selector Delegate selector - * @param column Column index to target - * @param callback Callback for when done + * @param n Element to bind the action to + * @param selector Selector (for delegated events) + * @param fn Callback function for when the event is triggered */ -function sortAttachListener(settings, node, selector, column, callback) { - bindAction(node, selector, function (e) { - var run = false; - var columns = column === undefined - ? columnsFromHeader(e.target) - : typeof column === 'function' - ? column() - : Array.isArray(column) - ? column - : [column]; - if (columns.length) { - for (var i = 0, iLen = columns.length; i < iLen; i++) { - var ret = sortAdd(settings, columns[i], i, e.shiftKey); - if (ret !== false) { - run = true; - } - // If the first entry is no sort, then subsequent - // sort columns are ignored - if (settings.order.length === 1 && - settings.order[0][1] === '') { - break; - } - } - if (run) { - processingRun(settings, true, function () { - sort(settings); - sortDisplay(settings, settings.display); - reDraw(settings, false, false); - if (callback) { - callback(); - } - }); - } +function bindAction(n, selector, fn) { + Dom.s(n) + .on('click.DT', selector, function (e) { + fn(e); + }) + .on('keypress.DT', selector, function (e) { + if (e.which === 13) { + e.preventDefault(); + fn(e); } + }) + .on('selectstart.DT', selector, function () { + // Don't want a double click resulting in text selection + return false; }); } /** - * Sort the display array to match the master's order + * Register a callback function. Easily allows a callback function to be added + * to an array store of callback functions that can then all be called together. * - * @param settings DataTables context - * @param display The display array + * @param settings dataTables settings object + * @param store Name of the array storage for the callbacks in settings + * @param fn Function to be called back */ -function sortDisplay(settings, display) { - if (display.length < 2) { - return; - } - var master = settings.displayMaster; - var masterMap = {}; - var map = {}; - var i; - // Rather than needing an `indexOf` on master array, we can create a map - for (i = 0; i < master.length; i++) { - masterMap[master[i]] = i; - } - // And then cache what would be the indexOf from the display - for (i = 0; i < display.length; i++) { - map[display[i]] = masterMap[display[i]]; +function callbackReg(ctx, store, fn) { + if (fn) { + ctx.callbacks[store].push(fn); } - display.sort(function (a, b) { - // Short version of this function is simply `master.indexOf(a) - master.indexOf(b);` - return map[a] - map[b]; - }); } /** - * Convert the API variants that can be used for defining the order into our - * internal OrderColumn array. + * Fire callback functions and trigger events. Note that the loop over the + * callback array store is done backwards! Further note that you do not want to + * fire off triggers in time sensitive applications (for example cell creation) + * as its slow. * - * @param settings DataTable context object - * @param nestedSort Array to write the resolve values to - * @param sortItem Source object / array from user (It is really an `Order` - * but due to `aaSorting` being used for input and the internal structure - * it is currently any). - * @todo Split aaSorting into unresolved and resolved parameters (in state.ts as - * well) + * @param ctx DataTables settings object + * @param callbackArr Name of the array storage for the callbacks in the context + * @param eventName Name of the custom event to trigger. If null no trigger is + * fired + * @param args Array of arguments to pass to the callback function / trigger + * @param bubbles True if the event should bubble */ -function sortResolve(settings, nestedSort, sortItem // TODO typing -) { - var push = function (a) { - if (plainObject(a)) { - let orderIdx = a; - let orderName = a; - if (orderIdx.idx !== undefined) { - // Index based ordering - nestedSort.push([orderIdx.idx, orderIdx.dir]); - } - else if (orderName.name) { - // Name based ordering - var cols = pluck(settings.columns, 'name'); - var idx = cols.indexOf(orderName.name); - if (idx !== -1) { - nestedSort.push([idx, orderName.dir]); - } - } - } - else { - // Plain column index and direction pair - nestedSort.push(a); - } - }; - if (plainObject(sortItem)) { - // Object - push(sortItem); - } - else if (Array.isArray(sortItem) && typeof sortItem[0] === 'number') { - // 1D array - push(sortItem); +function callbackFire(ctx, callbackArr, eventName, args, bubbles = false) { + var ret = []; + if (callbackArr) { + ret = ctx.callbacks[callbackArr] + .slice() + .reverse() + .map(function (val) { + return val.apply(ctx.instance, args); + }); } - else if (Array.isArray(sortItem)) { - // 2D array - for (var z = 0; z < sortItem.length; z++) { - push(sortItem[z]); // Object or array + if (eventName !== null) { + let table = Dom.s(ctx.table); + let result = table.trigger(eventName + '.dt', bubbles, args, { + dt: ctx.api + }); + // If not yet attached to the document, trigger the event + // on the body directly to sort of simulate the bubble + if (bubbles && table.closest('body').count() === 0) { + Dom.s('body').trigger(eventName + '.dt', bubbles, args, { + dt: ctx.api + }); } + ret.push(result[0]); } + return ret; } -function sortFlatten(settings) { - var i, k, kLen, aSort = [], extSort = ext.type.order, aoColumns = settings.columns, dataSort, colIdx, type, srcCol, fixed = settings.orderFixed, fixedObj = plainObject(fixed), nestedSort = []; - if (!settings.features.ordering) { - return aSort; - } - // Build the sort array, with pre-fix and post-fix options if they have been - // specified - if (Array.isArray(fixed)) { - sortResolve(settings, nestedSort, fixed); - } - if (fixedObj && fixed.pre) { - sortResolve(settings, nestedSort, fixed.pre); +function lengthOverflow(ctx) { + var start = ctx.displayStart, end = displayEnd(ctx), len = ctx.pageLength; + // If we have space to show extra rows (backing up from the end point - then + // do so + if (start >= end) { + start = end - len; } - sortResolve(settings, nestedSort, settings.order); - if (fixedObj && fixed.post) { - sortResolve(settings, nestedSort, fixed.post); + // Keep the start record on the current page + start -= start % len; + if (len === -1 || start < 0) { + start = 0; } - for (i = 0; i < nestedSort.length; i++) { - srcCol = nestedSort[i][0]; - if (aoColumns[srcCol]) { - dataSort = aoColumns[srcCol].orderData; - for (k = 0, kLen = dataSort.length; k < kLen; k++) { - colIdx = dataSort[k]; - type = aoColumns[colIdx].type || 'string'; - if (nestedSort[i]._idx === undefined) { - nestedSort[i]._idx = aoColumns[colIdx].orderSequence.indexOf(nestedSort[i][1]); - } - if (nestedSort[i][1]) { - aSort.push({ - src: srcCol, - col: colIdx, - dir: nestedSort[i][1], - index: nestedSort[i]._idx, - type: type, - formatter: extSort[type + '-pre'], - sorter: extSort[type + '-' + nestedSort[i][1]] - }); - } - } - } + ctx.displayStart = start; +} +/** + * Detect the data source being used for the table. Used to simplify the code a + * little (ajax) and to make it compress a little smaller. + * + * @param ctx DataTables settings object + * @returns Data source + */ +function dataSource(ctx) { + if (ctx.features.serverSide) { + return 'ssp'; } - return aSort; + else if (ctx.ajax) { + return 'ajax'; + } + return 'dom'; } /** - * Change the order of the table + * Common replacement for language strings * * @param ctx DataTables settings object - * @param col Column to perform sort on - * @param dir Direction to sort on + * @param str String with values to replace + * @param entries Plural number for _ENTRIES_ - can be undefined + * @returns String */ -function sort(ctx, col, dir) { - var i, iLen, aiOrig = [], extSort = ext.type.order, data = ctx.data, sortCol, displayMaster = ctx.displayMaster, aSort; - // Make sure the columns all have types defined - columnTypes(ctx); - // Allow a specific column to be sorted, which will _not_ alter the display - // master - if (col !== undefined) { - var srcCol = ctx.columns[col]; - aSort = [ - { - src: col, - col: col, - dir: dir || '', - index: 0, - type: srcCol.type, - formatter: extSort[srcCol.type + '-pre'], - sorter: extSort[srcCol.type + '-' + dir] - } - ]; - displayMaster = displayMaster.slice(); +function macros(ctx, str, entries) { + // When infinite scrolling, we are always starting at 1. _iDisplayStart is + // used only internally + var formatter = ctx.formatNumber, start = ctx.displayStart + 1, len = ctx.pageLength, vis = recordsDisplay(ctx), max = recordsTotal(ctx), all = len === -1; + return str + .replace(/_START_/g, formatter(start, ctx)) + .replace(/_END_/g, formatter(displayEnd(ctx), ctx)) + .replace(/_MAX_/g, formatter(max, ctx)) + .replace(/_TOTAL_/g, formatter(vis, ctx)) + .replace(/_PAGE_/g, formatter(all ? 1 : Math.ceil(start / len), ctx)) + .replace(/_PAGES_/g, formatter(all ? 1 : Math.ceil(vis / len), ctx)) + .replace(/_ENTRIES_/g, ctx.api.i18n('entries', '', entries)) + .replace(/_ENTRIES-MAX_/g, ctx.api.i18n('entries', '', max)) + .replace(/_ENTRIES-TOTAL_/g, ctx.api.i18n('entries', '', vis)); +} +/** + * Add elements to an array as quickly as possible, but stack safe. + * + * @param arr Array to add the data to + * @param data Data array that is to be added + */ +function arrayApply(arr, data) { + if (!data) { + return; + } + // Chrome can throw a max stack error if apply is called with + // too large an array, but apply is faster. + if (data.length < 10000) { + arr.push.apply(arr, data); } else { - aSort = sortFlatten(ctx); + for (var i = 0; i < data.length; i++) { + arr.push(data[i]); + } } - for (i = 0, iLen = aSort.length; i < iLen; i++) { - sortCol = aSort[i]; - // Load the data needed for the sort, for each cell - sortData(ctx, sortCol.col); +} +/** + * Add one or more listeners to the table + * + * @param that JQ for the table + * @param name Event name + * @param src Listener(s) + */ +function listener(that, name, src) { + let srcArr = Array.isArray(src) ? src : [src]; + for (var i = 0; i < srcArr.length; i++) { + that.on(name + '.dt.DT', srcArr[i]); } - /* No sorting required if server-side or no sorting array */ - if (dataSource(ctx) != 'ssp' && aSort.length !== 0) { - // Reset the initial positions on each pass so we get a stable sort - for (i = 0, iLen = displayMaster.length; i < iLen; i++) { - aiOrig[i] = i; +} +/** + * Escape HTML entities in strings, in an object + */ +function escapeObject(obj) { + if (ext.escape.attributes) { + each(obj, function (key, val) { + obj[key] = escapeHtml(val); + }); + } + return obj; +} + +const store = { + className: {}, + detect: [], + render: {}, + search: {}, + order: {} +}; +// Common function to remove new lines, strip HTML and diacritic control +function _filterString(stripHtml, normalize) { + return function (str) { + if (util.is.empty(str) || typeof str !== 'string') { + return str; } - // If the first sort is desc, then reverse the array to preserve original - // order, just in reverse - if (aSort.length && aSort[0].dir === 'desc' && ctx.orderDescReverse) { - aiOrig.reverse(); + str = str.replace(util.regex.reNewLines, ' '); + if (stripHtml) { + str = util.stripHtml(str); } - /* Do the sort - here we want multi-column sorting based on a given data source (column) - * and sorting function (from oSort) in a certain direction. It's reasonably complex to - * follow on its own, but this is what we want (example two column sorting): - * fnLocalSorting = function(a,b){ - * var test; - * test = oSort['string-asc']('data11', 'data12'); - * if (test !== 0) - * return test; - * test = oSort['numeric-desc']('data21', 'data22'); - * if (test !== 0) - * return test; - * return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); - * } - * Basically we have a test for each sorting column, if the data in that column is equal, - * test the next column. If all columns match, then we use a numeric sort on the row - * positions in the original data array to provide a stable sort. - */ - displayMaster.sort(function (a, b) { - var _a, _b; - var x, y, k, test, sortItem, len = aSort.length, dataA = (_a = data[a]) === null || _a === void 0 ? void 0 : _a.orderCache, dataB = (_b = data[b]) === null || _b === void 0 ? void 0 : _b.orderCache; - for (k = 0; k < len; k++) { - sortItem = aSort[k]; - // Data, which may have already been through a `-pre` function - x = dataA[sortItem.col]; - y = dataB[sortItem.col]; - if (sortItem.sorter) { - // If there is a custom sorter (`-asc` or `-desc`) for this - // data type, use it - test = sortItem.sorter(x, y); - if (test !== 0) { - return test; - } - } - else { - // Otherwise, use generic sorting - test = x < y ? -1 : x > y ? 1 : 0; - if (test !== 0) { - return sortItem.dir === 'asc' ? test : -test; - } - } - } - x = aiOrig[a]; - y = aiOrig[b]; - return x < y ? -1 : x > y ? 1 : 0; - }); + { + str = util.diacritics(str, false); + } + return str; + }; +} +function __numericReplace(d, decimalPlace, re1, re2) { + if (d !== 0 && (!d || d === '-')) { + return -Infinity; } - else if (aSort.length === 0) { - // Apply index order - displayMaster.sort(function (x, y) { - return x < y ? -1 : x > y ? 1 : 0; - }); + if (typeof d === 'number' || typeof d === 'bigint') { + return d; } - if (col === undefined) { - // Tell the draw function that we have sorted the data - ctx.wasOrdered = true; - ctx.sortDetails = aSort; - callbackFire(ctx, null, 'order', [ctx, aSort]); + // If a decimal place other than `.` is used, it needs to be given to the + // function so we can detect it and replace with a `.` which is the only + // decimal place JavaScript recognises - it is not locale aware. + if (decimalPlace) { + d = util.conv.numToDecimal(d, decimalPlace); } - return displayMaster; + if (typeof d === 'string') { + if (re1) { + d = d.replace(re1, ''); + } + if (re2) { + d = d.replace(re2, ''); + } + } + return d * 1; } -/** - * Function to run on user sort request - * - * @param settings dataTables settings object - * @param colIdx column sorting index - * @param addIndex Counter - * @param shift Shift click add - */ -function sortAdd(settings, colIdx, addIndex, shift) { - var col = settings.columns[colIdx]; - var sorting = settings.order; - var asSorting = col.orderSequence; - var nextSortIdx; - var next = function (a, overflow) { - var idx = a._idx; - if (idx === undefined) { - idx = asSorting.indexOf(a[1]); +function register$1(name, prop, val) { + if (!prop) { + return { + className: store.className[name], + detect: store.detect.find(function (fn) { + return fn._name === name; + }), + order: { + pre: store.order[name + '-pre'], + asc: store.order[name + '-asc'], + desc: store.order[name + '-desc'] + }, + render: store.render[name], + search: store.search[name] + }; + } + var setProp = function (prop2, propVal) { + store[prop2][name] = propVal; + }; + var setDetect = function (detect) { + // `detect` can be a function or an object - we set a name + // property for either - that is used for the detection + Object.defineProperty(detect, '_name', { value: name }); + var idx = store.detect.findIndex(function (item) { + return item._name === name; + }); + if (idx === -1) { + store.detect.unshift(detect); + } + else { + store.detect.splice(idx, 1, detect); } - return idx + 1 < asSorting.length ? idx + 1 : overflow ? null : 0; }; - if (!col.orderable) { - return false; + var setOrder = function (obj) { + store.order[name + '-pre'] = obj.pre; // can be undefined + store.order[name + '-asc'] = obj.asc; // can be undefined + store.order[name + '-desc'] = obj.desc; // can be undefined + }; + // prop is optional + if (val === undefined) { + val = prop; + prop = undefined; } - // Convert to 2D array if needed - if (typeof sorting[0] === 'number') { - sorting = settings.order = [sorting]; + if (prop === 'className') { + setProp('className', val); } - // If appending the sort then we are multi-column sorting - if ((shift || addIndex) && settings.features.orderMulti) { - // Are we already doing some kind of sort on this column? - var sortIdx = pluck(sorting, '0').indexOf(colIdx); - if (sortIdx !== -1) { - // Yes, modify the sort - nextSortIdx = next(sorting[sortIdx], true); - if (nextSortIdx === null && sorting.length === 1) { - nextSortIdx = 0; // can't remove sorting completely - } - if (nextSortIdx === null || asSorting[nextSortIdx] === '') { - sorting.splice(sortIdx, 1); - } - else { - sorting[sortIdx][1] = asSorting[nextSortIdx]; - sorting[sortIdx]._idx = nextSortIdx; - } + else if (prop === 'detect') { + setDetect(val); + } + else if (prop === 'order') { + setOrder(val); + } + else if (prop === 'render') { + setProp('render', val); + } + else if (prop === 'search') { + setProp('search', val); + } + else if (!prop) { + if (val.className) { + setProp('className', val.className); } - else if (shift) { - // No sort on this column yet, being added by shift click - // add it as itself - sorting.push([colIdx, asSorting[0], 0]); - sorting[sorting.length - 1]._idx = 0; + if (val.detect !== undefined) { + setDetect(val.detect); } - else { - // No sort on this column yet, being added from a colspan - // so add with same direction as first column - sorting.push([colIdx, sorting[0][1], 0]); - sorting[sorting.length - 1]._idx = 0; + if (val.order) { + setOrder(val.order); } - } - else if (sorting.length && sorting[0][0] == colIdx) { - // Single column - already sorting on this column, modify the sort - nextSortIdx = next(sorting[0]); - if (nextSortIdx) { - sorting.length = 1; - sorting[0][1] = asSorting[nextSortIdx]; - sorting[0]._idx = nextSortIdx; + if (val.render !== undefined) { + setProp('render', val.render); } - else { - sorting.length = 1; - sorting[0][1] = asSorting[0]; - sorting[0]._idx = 0; + if (val.search !== undefined) { + setProp('search', val.search); } } - else { - // Single column - sort only on this column - sorting.length = 0; - sorting.push([colIdx, asSorting[0]]); - sorting[0]._idx = 0; - } } -/** - * Set the sorting classes on table's body, Note: it is safe to call this function - * when bSort and bSortClasses are false - * - * @param settings DataTables settings object - */ -function sortingClasses(settings) { - var oldSort = settings.lastOrder; - var sortClass = settings.classes.order.position; - var sortFlat = sortFlatten(settings); - var features = settings.features; - var i, iLen, colIdx; - if (features.ordering && features.orderClasses) { - // Remove old sorting classes - for (i = 0, iLen = oldSort.length; i < iLen; i++) { - colIdx = oldSort[i].src; - // Remove column sorting - Dom.s(pluck(settings.data, 'cells', colIdx)).classRemove(sortClass + (i < 2 ? i + 1 : 3)); +// Get a list of types +function types() { + return store.detect.map(function (detect) { + return detect._name; + }); +} +var __diacriticSort = function (a, b) { + a = a !== null && a !== undefined ? a.toString().toLowerCase() : ''; + b = b !== null && b !== undefined ? b.toString().toLowerCase() : ''; + // Checked for `navigator.languages` support in `oneOf` so this code can't execute in old + // Safari and thus can disable this check + // eslint-disable-next-line compat/compat + return a.localeCompare(b, navigator.languages[0] || navigator.language, { + numeric: true, + ignorePunctuation: true + }); +}; +var __diacriticHtmlSort = function (a, b) { + a = util.stripHtml(a); + b = util.stripHtml(b); + return __diacriticSort(a, b); +}; +// +// Built in data types +// +register$1('string', { + detect: function () { + return 'string'; + }, + order: { + pre: function (a) { + // This is a little complex, but faster than always calling toString, + // http://jsperf.com/tostring-v-check + return util.is.empty(a) && typeof a !== 'boolean' + ? '' + : typeof a === 'string' + ? a.toLowerCase() + : !a.toString + ? '' + : a.toString(); } - // Add new column sorting - for (i = 0, iLen = sortFlat.length; i < iLen; i++) { - colIdx = sortFlat[i].src; - Dom.s(pluck(settings.data, 'cells', colIdx)).classAdd(sortClass + (i < 2 ? i + 1 : 3)); + }, + search: _filterString(false) +}); +register$1('string-utf8', { + detect: { + allOf: function () { + return true; + }, + oneOf: function (d) { + // At least one data point must contain a non-ASCII character + // This line will also check if navigator.languages is supported or not. If not (Safari 10.0-) + // this data type won't be supported. + // eslint-disable-next-line compat/compat + return (!util.is.empty(d) && + navigator.languages && + typeof d === 'string' && + !!d.match(/[^\x00-\x7F]/)); } - } - settings.lastOrder = sortFlat; -} -/** - * Get the data to sort a column, be it from cache, fresh (populating the - * cache), or from a sort formatter - * - * @param settings DataTables settings object - * @param colIdx Column index - */ -function sortData(settings, colIdx) { - // Custom sorting function - provided by the sort data type - var column = settings.columns[colIdx]; - var customSort = ext.order[column.orderDataType]; - var customData; - if (customSort) { - customData = customSort.call(settings.instance, settings, colIdx, columnIndexToVisible(settings, colIdx)); - } - // Use / populate cache - var row, cellData; - var formatter = ext.type.order[column.type + '-pre']; - var data = settings.data; - for (var rowIdx = 0; rowIdx < data.length; rowIdx++) { - // Sparse array - if (!data[rowIdx]) { - continue; + }, + order: { + asc: __diacriticSort, + desc: function (a, b) { + return __diacriticSort(a, b) * -1; } - row = data[rowIdx]; - if (row && !row.orderCache) { - row.orderCache = []; + }, + search: _filterString(false) +}); +register$1('html', { + detect: { + allOf: function (d) { + return (util.is.empty(d) || + (typeof d === 'string' && d.indexOf('<') !== -1)); + }, + oneOf: function (d) { + // At least one data point must contain a `<` + return (!util.is.empty(d) && + typeof d === 'string' && + d.indexOf('<') !== -1); + } + }, + order: { + pre: function (a) { + return util.is.empty(a) + ? '' + : a.replace + ? util.stripHtml(a).trim().toLowerCase() + : a + ''; + } + }, + search: _filterString(true) +}); +register$1('html-utf8', { + detect: { + allOf: function (d) { + return (util.is.empty(d) || + (typeof d === 'string' && d.indexOf('<') !== -1)); + }, + oneOf: function (d) { + // At least one data point must contain a `<` and a non-ASCII character + // eslint-disable-next-line compat/compat + return (navigator.languages && + !util.is.empty(d) && + typeof d === 'string' && + d.indexOf('<') !== -1 && + typeof d === 'string' && + !!d.match(/[^\x00-\x7F]/)); + } + }, + order: { + asc: __diacriticHtmlSort, + desc: function (a, b) { + return __diacriticHtmlSort(a, b) * -1; + } + }, + search: _filterString(true) +}); +register$1('date', { + className: 'dt-type-date', + detect: { + allOf: function (d) { + // V8 tries _very_ hard to make a string passed into `Date.parse()` + // valid, so we need to use a regex to restrict date formats. Use a + // plug-in for anything other than ISO8601 style strings + if (d && !(d instanceof Date) && !util.regex.reDate.test(d)) { + return null; + } + var parsed = Date.parse(d); + return (parsed !== null && !isNaN(parsed)) || util.is.empty(d); + }, + oneOf: function (d) { + // At least one entry must be a date or a string with a date + return (d instanceof Date || + (typeof d === 'string' && util.regex.reDate.test(d))); } - if (row && (!row.orderCache[colIdx] || customSort)) { - cellData = customSort - ? customData[rowIdx] // If there was a custom sort function, use data from there - : getCellData(settings, rowIdx, colIdx, 'sort'); - row.orderCache[colIdx] = formatter - ? formatter(cellData, settings) - : cellData; + }, + order: { + pre: function (d) { + var ts = Date.parse(d); + return isNaN(ts) ? -Infinity : ts; } } -} - -/** - * Alter the display settings to change the page - * - * @param settings DataTables settings object - * @param action Paging action to take: "first", "previous", "next" or "last" or - * page number to jump to (integer) - * @param redraw Automatically draw the update or not - * @returns true page has changed, false - no change - */ -function pageChange(settings, action, redraw) { - var start = settings.displayStart, len = settings.pageLength, records = recordsDisplay(settings); - if (records === 0 || len === -1) { - start = 0; - } - else if (typeof action === 'number') { - start = action * len; - if (start > records) { - start = 0; +}); +register$1('html-num-fmt', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, true, false); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, true, false); } - } - else if (action == 'first') { - start = 0; - } - else if (action == 'previous') { - start = len >= 0 ? start - len : 0; - if (start < 0) { - start = 0; + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp, util.regex.reHtml, util.regex.reFormattedNumeric); } - } - else if (action == 'next') { - if (start + len < records) { - start += len; + }, + search: _filterString(true) +}); +register$1('html-num', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, false, true); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, false, false); + } + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp, util.regex.reHtml); + } + }, + search: _filterString(true) +}); +register$1('num-fmt', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.num(d, decimal, true, true); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.num(d, decimal, true, false); + } + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp, util.regex.reFormattedNumeric); } } - else if (action == 'last') { - start = Math.floor((records - 1) / len) * len; - } - else if (action === 'ellipsis') { - return; - } - else { - log(settings, 0, 'Unknown paging action: ' + action, 5); - } - var changed = settings.displayStart !== start; - settings.displayStart = start; - callbackFire(settings, null, changed ? 'page' : 'page-nc', [settings]); - if (changed && redraw) { - draw(settings); +}); +register$1('num', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.num(d, decimal, false, true); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.num(d, decimal, false, false); + } + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp); + } } - return changed; -} +}); +/* + * Public helper functions. These aren't used internally by DataTables, or + * called by any of the options passed into DataTables, but they can be used + * externally by developers working with DataTables. They are helper functions + * to make working with DataTables a little bit easier. + */ /** - * State information for a table + * Common logic for moment, luxon or a date action. * - * @param settings DataTables settings object + * Happens after __mldObj, so don't need to call `resolveWindowsLibs` again */ -function saveState(settings) { - if (settings.loadingState) { - return; +function __mld(dtLib, momentFn, luxonFn, dateFn, arg1) { + if (__moment) { + return dtLib[momentFn](arg1); } - // Sort state saving uses [[idx, order]] structure. - var sorting = []; - sortResolve(settings, sorting, settings.order); - /* Store the interesting variables */ - var columns = settings.columns; - var state = { - columns: settings.columns.map(function (col, i) { - return { - name: col.name, - visible: col.visible, - search: Object.assign({}, settings.searches[i]) - }; - }), - length: settings.pageLength, - order: sorting.map(function (sort) { - // If a column name is available, use it - return columns[sort[0]] && columns[sort[0]].name - ? [columns[sort[0]].name, sort[1]] - : sort.slice(); - }), - search: Object.assign({}, settings.searches['*']), - searchGroups: Object.keys(settings.searches) - .filter(c => c.includes(',')) // Limit to only multi-column subsets - .map(c => Object.assign({}, settings.searches[c])), - start: settings.displayStart, - time: +new Date() - }; - settings.stateSaved = state; - callbackFire(settings, 'stateSaveParams', 'stateSaveParams', [ - settings, - state - ]); - if (settings.features.stateSave && !settings.destroying) { - settings.stateSaveCallback.call(settings.instance, settings, state); + else if (__luxon) { + return dtLib[luxonFn](arg1); } + return dateFn ? dtLib[dateFn](arg1) : dtLib; } +var __mlWarning = false; +var __luxon; +var __moment; /** - * Attempt to load a saved table state * - * @param settings dataTables settings object - * @param callback Callback to execute when the state has been loaded */ -function loadState(settings, callback) { - if (!settings.features.stateSave) { - callback(); - return; - } - var loaded = function (state, ignoreTime = false) { - implementState(settings, state, ignoreTime, callback); - }; - var state = settings.stateLoadCallback.call(settings.instance, settings, loaded); - if (state !== undefined) { - implementState(settings, state, false, callback); - } - // otherwise, wait for the loaded callback to be executed - return true; +function resolveWindowLibs() { + __luxon = util.external('luxon'); + __moment = util.external('moment'); } -function implementState(settings, s, ignoreTime, callback) { - var i, iLen; - var columns = settings.columns; - var currentNames = pluck(settings.columns, 'name'); - settings.loadingState = true; - // When StateRestore was introduced the state could now be implemented at - // any time Not just initialisation. To do this an api instance is required - // in some places - var api = settings.initDone ? new Api(settings) : null; - if (!ignoreTime) { - if (!s || !s.time) { - settings.loadingState = false; - callback(); - return; +function __mldObj(d, format, locale) { + var dt; + resolveWindowLibs(); + if (__moment) { + dt = __moment(d, format, locale, true); + if (!dt.isValid()) { + return null; } - // Reject old data - var duration = settings.stateDuration; - if (duration > 0 && s.time < +new Date() - duration * 1000) { - settings.loadingState = false; - callback(); - return; + } + else if (__luxon) { + dt = + format && typeof d === 'string' + ? __luxon.DateTime.fromFormat(d, format) + : __luxon.DateTime.fromISO(d); + if (!dt.isValid) { + return null; } + dt = dt.setLocale(locale); + } + else if (!format) { + // No format given, must be ISO + dt = new Date(d); } - // Allow custom and plug-in manipulation functions to alter the saved data - // set and cancelling of loading by returning false - var abStateLoad = callbackFire(settings, 'stateLoadParams', 'stateLoadParams', [settings, s]); - if (abStateLoad.indexOf(false) !== -1) { - settings.loadingState = false; - callback(); - return; + else { + if (!__mlWarning) { + alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17'); + } + __mlWarning = true; } - // Store the saved state so it might be accessed at any time - settings.stateLoaded = assignDeep({}, s); - // This is needed for ColReorder, which has to happen first to allow all - // the stored indexes to be usable. It is not publicly documented. - callbackFire(settings, null, 'stateLoadInit', [settings, s], true); - // Page Length - if (s.length !== undefined) { - // If already initialised just set the value directly so that the select - // element is also updated - if (api) { - api.page.len(s.length); + return dt; +} +// Wrapper for date, datetime and time which all operate the same way with the +// exception of the output string for auto locale support +function __mlHelper(localeString) { + return function (from, to, locale, def) { + // Luxon and Moment support + // Argument shifting + if (arguments.length === 0) { + locale = 'en'; + to = null; // means toLocaleString + from = null; // means iso8601 } - else { - settings.pageLength = s.length; + else if (arguments.length === 1) { + locale = 'en'; + to = from; + from = null; } - } - // Restore key features - if (s.start !== undefined) { - if (api === null) { - settings.displayStart = s.start; - settings.displayStartInit = s.start; + else if (arguments.length === 2) { + locale = to; + to = from; + from = null; } - else { - pageChange(settings, s.start / settings.pageLength); + var typeName = 'datetime' + (to ? '-' + to : ''); + // Add type detection and sorting specific to this date format - we need + // to be able to identify date type columns as such, rather than as + // numbers in extensions. Hence the need for this. + if (!store.order[typeName + '-pre']) { + register$1(typeName, { + detect: function (d) { + // The renderer will give the value to type detect as the + // type! + return d === typeName ? typeName : false; + }, + order: { + pre: function (d) { + // The renderer gives us Moment, Luxon or Date objects + // for the sorting, all of which have a `valueOf` which + // gives milliseconds epoch + return d.valueOf(); + } + } + }); } - } - // Order - if (s.order !== undefined) { - settings.order = []; - for (let i = 0; i < s.order.length; i++) { - let col = s.order[i]; - let set = [col[0], col[1]]; - // A column name was stored and should be used for restore - if (typeof col[0] === 'string') { - // Find the name from the current list of column names - let idx = currentNames.indexOf(col[0]); - if (idx < 0) { - // If the column was not found ignore it and continue - continue; + if (!store.className[typeName]) { + store.className[typeName] = 'dt-right'; + } + return function (d, type) { + // Allow for a default value + if (d === null || d === undefined) { + if (def === '--now') { + // We treat everything as UTC further down, so no changes + // are made, as such need to get the local date / time as if + // it were UTC + var local = new Date(); + d = new Date(Date.UTC(local.getFullYear(), local.getMonth(), local.getDate(), local.getHours(), local.getMinutes(), local.getSeconds())); + } + else { + d = ''; } - set[0] = idx; } - else if (set[0] >= columns.length) { - // If the column index is out of bounds ignore it and continue - continue; + if (type === 'type') { + // Typing uses the type name for fast matching + return typeName; + } + if (d === '') { + return type !== 'sort' + ? '' + : __mldObj('0000-01-01 00:00:00', null, locale); + } + // Shortcut. If `from` and `to` are the same, we are using the + // renderer to format for ordering, not display - its already in the + // display format. + if (to !== null && + from === to && + type !== 'sort' && + type !== 'type' && + !(d instanceof Date)) { + return d; + } + // Determine if there is a timezone. If there is, we want to reuse + // it for the output, so the timezone doesn't change between the + // input and output. + let options = {}; + let tzMatch = typeof d === 'string' ? d.match(util.regex.isoTimezone) : null; + if (tzMatch) { + options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1]; + } + // Get a Date object (Luxon, moment or Date) + var dt = __mldObj(d, from, locale); + if (dt === null) { + return d; + } + if (type === 'sort') { + return dt; + } + var formatted = to === null + ? __mld(dt, 'toDate', 'toJSDate', '')[localeString](navigator.language, options) + : __mld(dt, 'format', 'toFormat', 'toISOString', to); + // XSS protection + return type === 'display' ? util.escapeHtml(formatted) : formatted; + }; + }; +} +// Based on locale, determine standard number formatting +// Fallback for legacy browsers is US English +var __thousands = ','; +var __decimal = '.'; +if (window.Intl !== undefined) { + try { + var num = new Intl.NumberFormat().formatToParts(100000.1); + for (var i = 0; i < num.length; i++) { + if (num[i].type === 'group') { + __thousands = num[i].value; + } + else if (num[i].type === 'decimal') { + __decimal = num[i].value; } - settings.order.push(set); } } - // Search - if (s.search !== undefined) { - Object.assign(settings.searches['*'], s.search); + catch (e) { + // noop } - if (s.searchGroups) { - s.searchGroups.forEach(group => { - if (group.columns) { - let index = group.columns.join(','); - settings.searches[index] = create$2(group); - } - }); +} +/** + * Register a date / time format for DataTables to use. + * + * @param format The date / time format to detect data in. Please refer to the + * Moment.js or Luxon document for the full list of tokens, depending on which + * of the two libraries you are using. + * @param locale The locale to pass to Moment.js / Luxon. + */ +function datetime(format, locale) { + var typeName = 'datetime-' + format; + if (!locale) { + locale = 'en'; } - // Columns - if (s.columns) { - var set = s.columns; - var incoming = pluck(s.columns, 'name'); - // Check if it is a 2.2 style state object with a `name` property for - // the columns, and if the name was defined. If so, then create a new - // array that will map the state object given, to the current columns - // (don't bother if they are already matching tho). - if (incoming.join('').length && - incoming.join('') !== currentNames.join('')) { - set = []; - // For each column, try to find the name in the incoming array - for (i = 0; i < currentNames.length; i++) { - if (currentNames[i] != '') { - var idx = incoming.indexOf(currentNames[i]); - if (idx >= 0) { - set.push(s.columns[idx]); - } - else { - // No matching column name in the state's columns, so - // this might be a new column and thus can't have a - // state already. - set.push({}); - } - } - else { - // If no name, but other columns did have a name, then there - // is no knowing where this one came from originally so it - // can't be restored. - set.push({}); - } - } - } - // If the number of columns to restore is different from current, then - // all bets are off. - if (set.length === columns.length) { - for (i = 0, iLen = set.length; i < iLen; i++) { - var col = set[i]; - // Visibility - if (col.visible !== undefined) { - // If the api is defined, the table has been initialised so - // we need to use it rather than internal settings - if (api) { - // Don't redraw the columns on every iteration of this - // loop, we will do this at the end instead - api.column(i).visible(col.visible, false); - } - else { - columns[i].visible = col.visible; - } - } - // Search - if (col.search !== undefined) { - Object.assign(settings.searches[i], col.search); - // If out of order due to a change in order from named - // columns we need to make sure the index is correct - settings.searches[i].columns = [i]; - } - } - // If the api is defined then we need to adjust the columns once the - // visibility has been changed - if (api) { - api.one('draw', function () { - api.columns.adjust(); - }); + if (!store.order[typeName]) { + register$1(typeName, { + detect: function (d) { + var dt = __mldObj(d, format, locale); + return d === '' || dt ? typeName : false; + }, + order: { + pre: function (d) { + return __mldObj(d, format, locale) || 0; + } } - } + }); + } + if (!store.className[typeName]) { + store.className[typeName] = 'dt-right'; } - settings.loadingState = false; - callbackFire(settings, 'stateLoaded', 'stateLoaded', [settings, s]); - callback(); } - /** - * Draw the table for the first time, adding all required features - * - * @param settings DataTables settings object + * Helpers for `columns.render`. */ -function initialise(settings) { - var i; - var init = settings.init; - var deferLoading = settings.deferLoading; - var dataSrc = dataSource(settings); - // Ensure that the table data is fully initialised - if (!settings.initialised) { - setTimeout(function () { - initialise(settings); - }, 200); - return; - } - // Build the header / footer for the table - buildHead(settings, 'header'); - buildHead(settings, 'footer'); - // Load the table's state (if needed) and then render around it and draw - loadState(settings, function () { - // Then draw the header / footer - drawHead(settings, settings.header); - drawHead(settings, settings.footer); - // Cache the paging start point, as the first redraw will reset it - var iAjaxStart = settings.displayStartInit; - // Local data load - // Check if there is data passing into the constructor - if (init && init.data) { - for (i = 0; i < init.data.length; i++) { - addData(settings, init.data[i]); - } +var helpers = { + date: __mlHelper('toLocaleDateString'), + datetime: __mlHelper('toLocaleString'), + time: __mlHelper('toLocaleTimeString'), + number: function (thousands, decimal, precision, prefix, postfix) { + // Auto locale detection + if (thousands === null || thousands === undefined) { + thousands = __thousands; } - else if (deferLoading || dataSrc == 'dom') { - // Grab the data from the page - addTr(settings, Dom.s(settings.tbody).children('tr')); + if (decimal === null || decimal === undefined) { + decimal = __decimal; } - // Filter not yet applied - copy the display master - settings.display = settings.displayMaster.slice(); - // Enable features - createLayout(settings); - sortInit(settings); - colGroup(settings); - /* Okay to show that something is going on now */ - processingDisplay(settings, true); - callbackFire(settings, null, 'preInit', [settings], true); - // If there is default sorting required - let's do it. The sort function - // will do the drawing for us. Otherwise we draw the table regardless of - // the Ajax source - this allows the table to look initialised for Ajax - // sourcing data (show 'loading' message possibly) - reDraw(settings); - // Server-side processing init complete is done by _fnAjaxUpdateDraw - if (dataSrc != 'ssp' || deferLoading) { - // if there is an ajax source load the data - if (dataSrc == 'ajax') { - buildAjax(settings, {}, function (json) { - var aData = ajaxDataSrc(settings, json, false); - // Got the data - add it to the table - for (i = 0; i < aData.length; i++) { - addData(settings, aData[i]); - } - // Reset the init display for cookie saving. We've already - // done a filter, and therefore cleared it before. So we - // need to make it appear 'fresh' - settings.displayStartInit = iAjaxStart; - reDraw(settings); - processingDisplay(settings, false); - initComplete(settings); - }); - } - else { - initComplete(settings); - processingDisplay(settings, false); + return { + display: function (d) { + if (typeof d !== 'number' && typeof d !== 'string') { + return d; + } + if (d === '' || d === null) { + return d; + } + var flo = typeof d === 'number' ? d : parseFloat(d); + var negative = flo < 0 ? '-' : ''; + var abs = Math.abs(flo); + // Scientific notation for large and small numbers + if (abs >= 100000000000 || (abs < 0.0001 && abs !== 0)) { + var exp = flo.toExponential(precision).split(/e\+?/); + return exp[0] + ' x 10' + exp[1] + ''; + } + // If NaN then there isn't much formatting that we can do - just + // return immediately, escaping any HTML (this was supposed to + // be a number after all) + if (isNaN(flo)) { + return util.escapeHtml(d); + } + flo = flo.toFixed(precision); + var absPart = Math.abs(flo); + var intPart = Math.abs(parseInt(flo, 10)); + var floatPart = precision + ? decimal + + (absPart - intPart).toFixed(precision).substring(2) + : ''; + // If zero, then can't have a negative prefix + if (intPart === 0 && parseFloat(floatPart) === 0) { + negative = ''; + } + return (negative + + (prefix || '') + + intPart + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, thousands) + + floatPart + + (postfix || '')); } - } - }); -} + }; + }, + text: function () { + return { + display: util.escapeHtml, + filter: util.escapeHtml + }; + } +}; + /** - * Draw the table for the first time, adding all required features + * Column options that can be given to DataTables at initialisation time. + */ +const defaults$2 = { + ariaTitle: '', + cellType: 'td', + className: '', + contentPadding: '', + createdCell: null, + data: null, + defaultContent: null, + footer: null, + name: '', + orderable: true, + orderData: null, + orderDataType: 'std', + orderSequence: ['asc', 'desc', ''], + render: null, + search: null, + searchable: true, + title: null, + type: null, + visible: true, + width: null +}; + +/** + * Internal settings object used for individual columns. Instances are held in + * the setting object's `columns` array and contains all the information that + * DataTables needs about each individual column. * - * @param settings DataTables settings object + * Note that this object is related to the column defaults but this one is the + * internal data store for DataTables's cache of columns. It should NOT be + * manipulated outside of DataTables. Any configuration should be done through + * the initialisation options. */ -function initComplete(settings) { - if (settings.initDone) { - return; - } - var args = [settings, settings.json]; - settings.initDone = true; - // If the footer element is empty after initialisation, then remove it - let tfoot = Dom.s(settings.tfoot); - if (tfoot.children().count() === 0) { - tfoot.remove(); +class Settings { + constructor() { + /** + * Flag to indicate if HTML5 data attributes should be used as the data + * source for filtering or sorting. True is either are. + */ + this.attrSrc = false; + this.ariaTitle = ''; + /** + * The class to apply to all cells in the table's `tbody`` for the column + */ + this.className = null; + /** + * When DataTables calculates the column widths to assign to each column, it + * finds the longest string in each column and then constructs a temporary + * table and reads the widths from that. The problem with this is that "mmm" + * is much wider then "iiii", but the latter is a longer string - thus the + * calculation can go wrong (doing it properly and putting it into an DOM + * object and measuring that is horribly(!) slow). Thus as a "work around" + * we provide this option. It will append its value to the text that is + * found to be the longest string for the column - i.e. padding. + */ + this.contentPadding = null; + /** + * Property to read the value for the cells in the column from the data + * source array / object. If null, then the default content is used, if a + * function is given then the return from the function is used. + */ + this.data = null; + /** + * Allows a default value to be given for a column's data, and will be used + * whenever a null data source is encountered (this can be because mData is + * set to null, or because the data source itself is null). + */ + this.defaultContent = null; + /** + * Name for the column, allowing reference to the column by name as well as + * by index (needs a lookup to work by name). + */ + this.name = null; + /** + * A list of the columns that sorting should occur on when this column is + * sorted. That this property is an array allows multi-column sorting to be + * defined for a column (for example first name / last name columns would + * benefit from this). The values are integers pointing to the columns to be + * sorted on (typically it will be a single integer pointing at itself, but + * that doesn't need to be the case). + */ + this.orderData = []; + /** + * Custom sorting data type - defines which of the available plug-ins in + * afnSortData the custom sorting will use - if any is defined. + */ + this.orderDataType = 'std'; + /** + * Class to be applied to the header element when sorting on this column + */ + this.orderingClass = null; + /** + * Define the sorting directions that are applied to the column, in sequence + * as the column is repeatedly sorted upon - i.e. the first value is used as + * the sorting direction when the column if first sorted (clicked on). Sort + * it again (click again) and it will move on to the next index. Repeat + * until loop. + */ + this.orderSequence = []; + /** + * Partner property to mData which is used (only when defined) to get the + * data - i.e. it is basically the same as mData, but without the 'set' + * option, and also the data fed to it is the result from mData. This is the + * rendering method to match the data method of mData. + */ + this.render = null; + /** + * Title of the column - what is seen in the TH element (nTh). + */ + this.title = null; + /** + * Store for manual type assignment using the `column.type` option. This + * is held in store so we can manipulate the column's `type` property. + */ + this.typeManual = null; + /** Cached longest strings from a column */ + this.wideStrings = null; + /** + * Width of the column + */ + this.width = null; + /** + * Width of the column when it was first "encountered" + */ + this.widthOrig = null; } - // Table is fully set up and we have data, so calculate the - // column widths - adjustColumnSizing(settings); - callbackFire(settings, null, 'plugin-init', args, true); - callbackFire(settings, 'init', 'init', args, true); } +const browser = { + barWidth: -1, + scrollbarLeft: false +}; +const hungarianToCamelRe = /^(a|aa|ai|ao|as|b|fn|i|m|o|s)([A-Z])([a-z].*$)/; /** - * Create an Ajax call based on the table's settings, taking into account that - * parameters can have multiple forms, and backwards compatibility. - * - * @param settings DataTables settings object - * @param data Data to send to the server, required by DataTables - may be - * augmented by developer callbacks - * @param fn Callback function to run when data is obtained + * Take an object which has hungarian notation parameters and convert them to + * camelCase style. This is to allow compatibility with DataTables 1.9 and + * earlier which only used hungarian notation, and also with DataTables 1.10 - 2 + * which allowed it to be used. */ -function buildAjax(settings, data, fn) { - var ajaxData; - var ajaxConfig = settings.ajax; - var instance = settings.instance; - var callback = function (json) { - var status = settings.jqXHR ? settings.jqXHR.status : null; - if (json === null || (typeof status === 'number' && status == 204)) { - json = {}; - ajaxDataSrc(settings, json, []); - } - var error = json.error || json.sError; - if (error) { - log(settings, 0, error); - } - // Microsoft often wrap JSON as a string in another JSON object Let's - // handle that automatically - if (json.d && typeof json.d === 'string') { - try { - json = JSON.parse(json.d); - } - catch (e) { - // noop - } - } - settings.json = json; - callbackFire(settings, null, 'xhr', [settings, json, settings.jqXHR], true); - fn(json); - }; - if (util.is.plainObject(ajaxConfig) && ajaxConfig.data) { - ajaxData = ajaxConfig.data; - var newData = typeof ajaxData === 'function' - ? ajaxData(data, settings) // fn can manipulate data or return - : ajaxData; // an object or array to merge - // If the function returned something, use that alone - data = - typeof ajaxData === 'function' && newData - ? newData - : util.object.assignDeep(data, newData); - // Remove the data property as we've resolved it already and don't want - // jQuery to do it again (it is restored at the end of the function) - delete ajaxConfig.data; +function hungarianToCamel(user) { + if (!user) { + return user; } - var baseAjax = { - url: typeof ajaxConfig === 'string' ? ajaxConfig : '', - data: data, - success: callback, - dataType: 'json', - cache: false, - type: settings.serverMethod, - error: function (xhr, error) { - var ret = callbackFire(settings, null, 'xhr', [settings, null, settings.jqXHR], true); - if (ret.indexOf(false) === -1) { - if (error == 'parsererror') { - log(settings, 0, 'Invalid JSON response', 1); - } - else if (xhr.readyState === 4) { - log(settings, 0, 'Ajax error', 7); - } - } - processingDisplay(settings, false); + let userKeys = Object.keys(user); + let userAny = user; + for (let i = 0; i < userKeys.length; i++) { + let userKey = userKeys[i]; + let match = userKey.match(hungarianToCamelRe); + // Is the key in hungarian notation? + if (match) { + // If so map it down + user[match[2].toLowerCase() + match[3]] = userAny[userKey]; + } + // Recurse down through the object + if (util.is.plainObject(userAny[userKey])) { + hungarianToCamel(userAny[userKey]); } - }; - // If `ajax` option is an object, extend and override our default base - if (util.is.plainObject(ajaxConfig)) { - util.object.assign(baseAjax, ajaxConfig); - } - // Store the data submitted for the API - settings.ajaxData = data; - // Allow plug-ins and external processes to modify the data - callbackFire(settings, null, 'preXhr', [settings, data, baseAjax], true); - if (typeof ajaxConfig === 'function') { - // Is a function - let the caller define what needs to be done - settings.jqXHR = ajaxConfig.call(instance, data, callback, settings); - } - else if (ajaxConfig && - typeof ajaxConfig !== 'string' && - ajaxConfig.url === '') { - // No url, so don't load any data. Just apply an empty data array - // to the object for the callback. - var empty = {}; - ajaxDataSrc(settings, empty, []); - callback(empty); - } - else { - // Object to extend the base settings - settings.jqXHR = util.ajax(baseAjax); - } - // Restore for next time around - if (ajaxData) { - ajaxConfig.data = ajaxData; } + return user; } /** - * Update the table using an Ajax call + * Map one parameter onto another * - * @param settings DataTables settings object - * @returns Block the table drawing or not + * @param o Object to map + * @param newKey The new parameter name + * @param oldKey The old parameter name */ -function ajaxUpdate(settings) { - settings.drawCount++; - processingDisplay(settings, true); - buildAjax(settings, ajaxParameters(settings), function (json) { - ajaxUpdateDraw(settings, json); - }); -} -function functionOrValue(val) { - return typeof val === 'function' ? 'function' : val.toString(); +function compatMap(o, newKey, oldKey) { + if (o[oldKey] !== undefined) { + o[newKey] = o[oldKey]; + } } /** - * Build up the parameters in an object needed for a server-side processing - * request. + * Provide backwards compatibility for the main DT options. Note that the new + * options are mapped onto the old parameters, so this is an external interface + * change only. * - * @param settings DataTables settings object - * @returns Block the table drawing or not + * @param init Object to map + * @param defaults Indicate if the object is the defaults object or not */ -function ajaxParameters(settings) { - var columns = settings.columns, features = settings.features, searches = settings.searches, searchesFixed = settings.searchesFixed, colData = function (idx, prop) { - return typeof columns[idx][prop] === 'function' - ? 'function' - : columns[idx][prop]; - }; - return { - draw: settings.drawCount, - columns: columns.map(function (column, i) { - return { - data: colData(i, 'data'), - name: column.name, - searchable: column.searchable, - orderable: column.orderable, - search: { - value: searches[i] - ? functionOrValue(searches[i].search) - : '', - regex: searches[i] ? searches[i].regex : false, - fixed: searchesFixed[i] - ? Object.keys(searchesFixed[i]).map(name => ({ - name: name, - term: functionOrValue(searchesFixed[i][name].search) - })) - : [] - } - }; - }), - order: sortFlatten(settings).map(function (val) { - return { - column: val.col, - dir: val.dir, - name: colData(val.col, 'name') - }; - }), - start: settings.displayStart, - length: features.paging ? settings.pageLength : -1, - search: { - value: functionOrValue(searches['*'].search), - regex: searches['*'].regex, - fixed: Object.keys(settings.searchesFixed['*']).map(name => ({ - name: name, - term: functionOrValue(settings.searchesFixed['*'][name].search) - })), - groups: Object.keys(settings.searches) - .filter(c => c.includes(',')) // Limit to only multi-column subsets - .map(c => ({ - columns: settings.searches[c].columns || [], - term: functionOrValue(settings.searches[c].search) - })), - groupsFixed: Object.keys(settings.searchesFixed) - .filter(c => c.includes(',')) // Limit to only multi-column subsets - .map(c => { - let searches = settings.searchesFixed[c]; - return Object.keys(searches).map(n => ({ - columns: searches[n].columns || [], - name: n, - term: functionOrValue(searches[n].search) - })); - }) - .flat() +function compatOpts(init, defaults = false) { + // Convert any old style parameters to camelCase + hungarianToCamel(init); + // Map old parameter names to new + compatMap(init, 'ordering', 'sort'); + compatMap(init, 'orderMulti', 'sortMulti'); + compatMap(init, 'orderClasses', 'sortClasses'); + compatMap(init, 'orderCellsTop', 'sortCellsTop'); + compatMap(init, 'order', 'sorting'); + compatMap(init, 'orderFixed', 'sortingFixed'); + compatMap(init, 'paging', 'paginate'); + compatMap(init, 'pagingType', 'paginationType'); + compatMap(init, 'pageLength', 'displayLength'); + compatMap(init, 'searching', 'filter'); + compatMap(init, 'stateDuration', 'cookieDuration'); + // Boolean initialisation of x-scrolling + if (typeof init.scrollX === 'boolean') { + init.scrollX = init.scrollX ? '100%' : ''; + } + // Objects for ordering + if (typeof init.ordering === 'object') { + init.orderIndicators = + init.ordering.indicators !== undefined + ? init.ordering.indicators + : true; + init.orderHandler = + init.ordering.handler !== undefined ? init.ordering.handler : true; + if (!defaults) { + init.ordering = true; } - }; + } + else if (init.ordering === false) { + init.orderIndicators = false; + init.orderHandler = false; + } + else if (init.ordering === true) { + init.orderIndicators = true; + init.orderHandler = true; + } + // Which cells are the title cells? + if (typeof init.orderCellsTop === 'boolean') { + init.titleRow = init.orderCellsTop; + } + // Column search objects are in an array, so it needs to be converted + // element by element + var searchCols = init.searchCols; + if (searchCols) { + for (var i = 0, iLen = searchCols.length; i < iLen; i++) { + if (searchCols[i]) { + hungarianToCamel(searchCols[i]); + } + } + } + // Enable search delay if server-side processing is enabled + if (init.serverSide && !init.searchDelay) { + init.searchDelay = 400; + } + // Language + if (init.language && init.language.url && !init.language.ajax) { + init.language.ajax = init.language.url; + } } /** - * Data the data from the server (nuking the old) and redraw the table + * Provide backwards compatibility for column options. Note that the new options + * are mapped onto the old parameters, so this is an external interface change + * only. * - * @param settings DataTables settings object - * @param json json data return from the server. + * @param init Object to map */ -function ajaxUpdateDraw(settings, json) { - var data = ajaxDataSrc(settings, json, false); - var drawUnique = ajaxDataSrcParam(settings, 'draw', json); - var recordsTotal = ajaxDataSrcParam(settings, 'recordsTotal', json); - var recordsFiltered = ajaxDataSrcParam(settings, 'recordsFiltered', json); - var existingTypes = settings.columns.map(c => c.type).join(','); - if (drawUnique !== undefined) { - // Protect against out of sequence returns - if (drawUnique * 1 < settings.drawCount) { - return; - } - settings.drawCount = drawUnique * 1; +function compatCols(init) { + // Convert any old style parameters to camelCase + hungarianToCamel(init); + // typeof columnDefaults + compatMap(init, 'orderable', 'sortable'); + compatMap(init, 'orderData', 'dataSort'); + compatMap(init, 'orderSequence', 'sorting'); + compatMap(init, 'orderDataType', 'sortDataType'); + compatMap(init, 'className', 'class'); + // orderData can be given as an integer + var dataSort = init.aDataSort; + var orderData = init.orderData; + if (typeof dataSort === 'number') { + init.orderData = [dataSort]; } - // No data in returned object, so rather than an array, we show an empty - // table - if (!data) { - data = []; + if (typeof orderData === 'number') { + init.orderData = [orderData]; } - clearTable(settings); - settings.recordsTotal = parseInt(recordsTotal, 10); - settings.recordsDisplay = parseInt(recordsFiltered, 10); - for (var i = 0, iLen = data.length; i < iLen; i++) { - addData(settings, data[i]); + // Backwards compatibility for mDataProp from 1.9- + if (init.dataProp !== undefined && !init.data) { + init.data = init.dataProp; } - settings.display = settings.displayMaster.slice(); - columnTypes(settings, existingTypes); - draw(settings, true); - initComplete(settings); - processingDisplay(settings, false); } /** - * Get the data from the JSON data source to use for drawing a table. + * Browser feature detection for capabilities, quirks * - * @param settings DataTables settings object - * @param json Data source object / array from the server - * @param write Array or object to write the data to - * @return Array of data to use + * @param ctx DataTables settings object */ -function ajaxDataSrc(settings, json, write) { - var dataProp = 'data'; - if (util.is.plainObject(settings.ajax) && - settings.ajax.dataSrc !== undefined) { - // Could in inside a `dataSrc` object, or not! - var dataSrc = settings.ajax.dataSrc; - // string, function and object are valid types - if (typeof dataSrc === 'string' || typeof dataSrc === 'function') { - dataProp = dataSrc; - } - else if (dataSrc.data !== undefined) { - dataProp = dataSrc.data; - } - } - if (!write) { - if (dataProp === 'data') { - // If the default, then we still want to support the old style, and - // safely ignore it if possible - return json.aaData || json[dataProp]; - } - return dataProp !== '' ? util.get(dataProp)(json) : json; +function browserDetect(ctx) { + // We don't need to do this every time DataTables is constructed, the values + // calculated are specific to the browser and OS configuration which we + // don't expect to change between initialisations + if (browser.barWidth === -1) { + // Scrolling feature / quirks detection + var n = Dom + .c('div') + .css({ + position: 'fixed', + top: '0', + left: -1 * window.pageXOffset + 'px', // allow for scrolling + height: '1px', + width: '1px', + overflow: 'hidden' + }) + .append(Dom + .c('div') + .css({ + position: 'absolute', + top: '1px', + left: '1px', + width: '100px', + overflow: 'scroll' + }) + .append(Dom.c('div').css({ + width: '100%', + height: '10px' + }))) + .appendTo('body'); + var outer = n.children(); + var inner = outer.children(); + browser.barWidth = outer.get(0).offsetWidth - outer.get(0).clientWidth; + browser.scrollbarLeft = Math.round(inner.offset().left) !== 1; + n.remove(); } - // set - util.set(dataProp)(json, write); + Object.assign(ctx.browser, browser); + ctx.scroll.barWidth = browser.barWidth; } + /** - * Very similar to ajaxDataSrc, but for the other SSP properties + * Add a column to the list used for the table with default values * * @param settings DataTables settings object - * @param param Target parameter - * @param json JSON data - * @returns Resolved value */ -function ajaxDataSrcParam(settings, param, json) { - var dataSrc = util.is.plainObject(settings.ajax) - ? settings.ajax.dataSrc // TODO - : null; - if (dataSrc && dataSrc[param]) { - // Get from custom location - return util.data.get(dataSrc[param])(json); +function addColumn(settings) { + // Add column to aoColumns array + let columnIdx = settings.columns.length; + let column = util.object.assign({}, new Settings(), defaults$2, { + orderData: defaults$2.orderData + ? defaults$2.orderData + : [columnIdx], + data: defaults$2.data ? defaults$2.data : columnIdx, + idx: columnIdx, + searchFixed: {}, + colEl: Dom + .c('col') + .attr('data-dt-column', columnIdx) + }); + settings.columns.push(column); + // Legacy support for `searchCols` property. If set, and there is a value + // for this column, then it should be applied to the search. The new, column + // specific `search` option is applied in `columnOptions`, but we always + // want the search object for the column to exist. + let searchCols = settings.searchCols; + settings.searches[columnIdx] = create$1(searchCols[columnIdx] + ? hungarianToCamel(searchCols[columnIdx]) + : {}); + settings.searches[columnIdx].columns = [columnIdx]; +} +/** + * Apply options for a column + * + * @param settings DataTables settings object + * @param colIdx column index to consider + * @param options Column configuration options + */ +function columnOptions(settings, colIdx, options) { + var column = settings.columns[colIdx]; + /* User specified column options */ + if (options !== undefined && options !== null) { + // Backwards compatibility + compatCols(options); + if (options.type) { + column.typeManual = options.type; + } + // `class` is a reserved word in JavaScript, so we need to provide + // the ability to use a valid name for the camel case input + if (options.className && !options.className) { + options.className = options.className; + } + var origClass = column.className; + util.object.assign(column, options); + map(column, options, 'width', 'widthOrig'); + // Merge class from previously defined classes with this one, rather + // than just overwriting it in the extend above + if (origClass !== column.className) { + column.className = origClass + ' ' + column.className; + } + map(column, options, 'orderData'); + // Search term specifically for this column + if (options.search) { + util.object.assign(settings.searches[colIdx], options.search); + } } - // else - Default behaviour - var old = ''; - // Legacy support - if (param === 'draw') { - old = 'sEcho'; + /* Cache the data get and set functions for speed */ + var dataSrc = column.data; + var dataFn = util.get(dataSrc); + // The `render` option can be given as an array to access the helper + // rendering methods. The first element is the rendering method to use, the + // rest are the parameters to pass + if (column.render && Array.isArray(column.render)) { + var copy = column.render.slice(); + var name = copy.shift(); + column.render = helpers[name].apply(window, copy); } - else if (param === 'recordsTotal') { - old = 'iTotalRecords'; + column.renderer = column.render ? util.get(column.render) : null; + var attrTest = function (src) { + return typeof src === 'string' && src.indexOf('@') !== -1; + }; + column.attrSrc = + !!dataSrc && + util.is.plainObject(dataSrc) && + (attrTest(dataSrc.sort) || + attrTest(dataSrc.type) || + attrTest(dataSrc.filter)); + column.setter = null; + column.dataGet = function (rowData, type, meta) { + var innerData = dataFn(rowData, type, undefined, meta); + return column.renderer && type + ? column.renderer(innerData, type, rowData, meta) + : innerData; + }; + column.dataSet = function (rowData, val, meta) { + return util.set(dataSrc)(rowData, val, meta); + }; + // Indicate if DataTables should read DOM data as an object or array + // Used in _fnGetRowElements + if (typeof dataSrc !== 'number' && !column._isArrayHost) { + settings.rowReadObject = true; } - else if (param === 'recordsFiltered') { - old = 'iTotalDisplayRecords'; + // Feature sorting overrides column specific when off + if (!settings.features.ordering) { + column.orderable = false; } - return json[old] !== undefined ? json[old] : json[param]; } - -const __filter_div = Dom.c('div').get(0); -const __filter_div_textContent = __filter_div.textContent !== undefined; /** - * Filter the table using both the global filter and column based filtering + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! * * @param settings DataTables settings object */ -function filterComplete(settings) { - settings.columns; - // In server-side processing all filtering is done by the server, so no - // point hanging around here - if (dataSource(settings) != 'ssp') { - // Check if any of the rows were invalidated - filterData(settings); - // Start from the full data set - settings.display = settings.displayMaster.slice(); - // Column set filters first - util.object.each(settings.searches, (key, s) => { - filter(settings.display, settings, s.search, s); - }); - // Fixed (named) filters next - util.object.each(settings.searchesFixed, function (columns) { - util.object.each(settings.searchesFixed[columns], function (name, s) { - filter(settings.display, settings, s.search, s); - }); - }); - // And finally legacy global filtering - filterCustom(settings); +function adjustColumnSizing(settings) { + calculateColumnWidths(settings); + columnSizes(settings); + let scroll = settings.scroll; + if (scroll.y !== '' || scroll.x !== '') { + scrollDraw(settings); } - // Tell the draw function we have been filtering - settings.wasFiltered = true; - callbackFire(settings, null, 'search', [settings]); + callbackFire(settings, null, 'column-sizing', [settings]); } /** - * Apply custom filtering functions - * - * This is legacy now that we have named functions, but it is widely used - * from 1.x, so it is not yet deprecated. + * Apply column sizes * * @param settings DataTables settings object */ -function filterCustom(settings) { - let filters = ext.search; - let displayRows = settings.display; - let row, rowIdx; - for (let i = 0, iLen = filters.length; i < iLen; i++) { - let rows = []; - // Loop over each row and see if it should be included - for (let j = 0, jen = displayRows.length; j < jen; j++) { - rowIdx = displayRows[j]; - row = settings.data[rowIdx]; - if (row && - filters[i](settings, row.searchCellCache, rowIdx, row.data, j)) { - rows.push(rowIdx); +function columnSizes(settings) { + let cols = settings.columns; + for (let i = 0; i < cols.length; i++) { + let width = columnsSumWidth(settings, [i], false); + if (width) { + cols[i].colEl.css('width', width); + if (settings.scroll.x) { + cols[i].colEl.css('min-width', width); } } - // So the array reference doesn't break set the results into the - // existing array - displayRows.length = 0; - arrayApply(displayRows, rows); } } /** - * Filter the data table based on user input and draw the table + * Convert the index of a visible column to the index in the data array (take + * account of hidden columns) * - * @param searchRows - * @param settings - * @param input - * @param options - * @returns - */ -function filter(searchRows, settings, input, options) { - if (input === '') { - return; - } - let i = 0; - let matched = []; - // Search term can be a function, regex or string - if a string we apply our - // smart filtering regex (assuming the options require that) - let searchFunc = typeof input === 'function' ? input : null; - let rpSearch = input instanceof RegExp - ? input - : searchFunc - ? null - : filterCreateSearch(input, options); - let columns = options.columns - ? options.columns - : util.array.range(settings.columns.length); - // Then for each row, does the test pass. If not, lop the row from the array - for (i = 0; i < searchRows.length; i++) { - let row = settings.data[searchRows[i]]; - if (row) { - // Get the data array based on the columns to include in the search - let data = util.array.selectiveJoin(row.searchCellCache, columns); - // Run the search action - if ((searchFunc && - searchFunc(data, row.data, searchRows[i], columns.length === 1 ? columns[0] : columns // compat - )) || - (rpSearch && typeof data === 'string' && rpSearch.test(data))) { - matched.push(searchRows[i]); - } - } - } - // Mutate the searchRows array - searchRows.length = matched.length; - for (i = 0; i < matched.length; i++) { - searchRows[i] = matched[i]; - } -} -/** - * Build a regular expression object suitable for searching a table - */ -function filterCreateSearch(searchIn, inOpts) { - let not = []; - let options = Object.assign({}, { - boundary: false, - caseInsensitive: true, - exact: false, - regex: false, - smart: true - }, inOpts); - let search = typeof searchIn !== 'string' ? searchIn.toString() : searchIn; - // Remove diacritics if normalize is set up to do so - search = util.diacritics(search); - if (options.exact) { - return new RegExp('^' + util.escapeRegex(search) + '$', options.caseInsensitive ? 'i' : ''); - } - search = options.regex ? search : util.escapeRegex(search); - if (options.smart) { - /* For smart filtering we want to allow the search to work regardless of - * word order. We also want double quoted text to be preserved, so word - * order is important - a la google. And a negative look around for - * finding rows which don't contain a given string. - * - * So this is the sort of thing we want to generate: - * - * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$ - */ - let parts = search.match(/!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g) || [ - '' - ]; - let a = parts.map(function (word) { - let negative = false; - let m; - // Determine if it is a "does not include" - if (word.charAt(0) === '!') { - negative = true; - word = word.substring(1); - } - // Strip the quotes from around matched phrases - if (word.charAt(0) === '"') { - m = word.match(/^"(.*)"$/); - word = m ? m[1] : word; - } - else if (word.charAt(0) === '\u201C') { - // Smart quote match (iPhone users) - m = word.match(/^\u201C(.*)\u201D$/); - word = m ? m[1] : word; - } - // For our "not" case, we need to modify the string that is - // allowed to match at the end of the expression. - if (negative) { - if (word.length > 1) { - not.push('(?!' + word + ')'); - } - word = ''; - } - return word.replace(/"/g, ''); - }); - let match = not.length ? not.join('') : ''; - let boundary = options.boundary ? '\\b' : ''; - search = - '^(?=.*?' + - boundary + - a.join(')(?=.*?' + boundary) + - ')(' + - match + - '.)*$'; - } - return new RegExp(search, options.caseInsensitive ? 'i' : ''); -} -// Update the filtering data for each row if needed (by invalidation or first -// run) -function filterData(settings) { - let columns = settings.columns; - let data = settings.data; - let column; - let j, jen, cellData, row; - let wasInvalidated = false; - for (let rowIdx = 0; rowIdx < data.length; rowIdx++) { - if (!data[rowIdx]) { - continue; - } - row = data[rowIdx]; - if (row && !row.searchCellCache) { - const rowFilterData = []; - for (j = 0, jen = columns.length; j < jen; j++) { - column = columns[j]; - if (column.searchable) { - cellData = getCellData(settings, rowIdx, j, 'filter'); - // Search in DataTables is string based - if (cellData === null) { - cellData = ''; - } - if (typeof cellData !== 'string' && cellData.toString) { - cellData = cellData.toString(); - } - } - else { - cellData = ''; - } - // If it looks like there is an HTML entity in the string, - // attempt to decode it so sorting works as expected. Note that - // we could use a single line of jQuery to do this, but the DOM - // method used here is much faster - // https://jsperf.com/html-decode - if (cellData.indexOf && cellData.indexOf('&') !== -1) { - __filter_div.innerHTML = cellData; - cellData = __filter_div_textContent - ? __filter_div.textContent - : __filter_div.innerText; - } - if (cellData.replace) { - cellData = cellData.replace(/[\r\n\u2028]/g, ''); - } - rowFilterData.push(cellData); + * @param settings DataTables settings object + * @param visIdx Visible column index to lookup + * @returns i the data index + */ +function visibleToColumnIndex(settings, visIdx) { + let aiVis = getColumns(settings, 'visible'); + return typeof aiVis[visIdx] === 'number' ? aiVis[visIdx] : null; +} +/** + * Convert the index of an index in the data array and convert it to the visible + * column index (take account of hidden columns) + * + * @param settings DataTables settings object + * @param match Column index to lookup + * @returns The data index + */ +function columnIndexToVisible(settings, match) { + let aiVis = getColumns(settings, 'visible'); + let iPos = aiVis.indexOf(match); + return iPos !== -1 ? iPos : null; +} +/** + * Get the number of visible columns + * + * @param settings DataTables settings object + * @returns i the number of visible columns + */ +function visibleColumns(settings) { + let layout = settings.header; + let columns = settings.columns; + let vis = 0; + if (layout.length) { + for (let i = 0, iLen = layout[0].length; i < iLen; i++) { + if (columns[i].visible && + Dom.s(layout[0][i].cell).css('display') !== 'none') { + vis++; } - row.searchCellCache = rowFilterData; - row.searchRowCache = rowFilterData.join(' '); - wasInvalidated = true; } } - return wasInvalidated; + return vis; } - /** - * Render and cache a row's display data for the columns, if required + * Get an array of column indexes that match a given property * * @param settings DataTables settings object - * @param rowIdx Row index - * @returns Array with display information + * @param param Parameter in the columns array to look for + * @returns Array of indexes with matched properties */ -function getRowDisplay(settings, rowIdx) { - var rowModal = settings.data[rowIdx]; - var columns = settings.columns; - if (!rowModal) { - return []; - } - if (!rowModal.displayData) { - // Need to render and cache - rowModal.displayData = []; - for (var colIdx = 0, len = columns.length; colIdx < len; colIdx++) { - rowModal.displayData.push(getCellData(settings, rowIdx, colIdx, 'display')); +function getColumns(settings, param) { + let a = []; + settings.columns.map(function (val, i) { + if (val[param]) { + a.push(i); } - } - return rowModal.displayData; + }); + return a; } /** - * Create a new TR element (and it's TD children) for a row + * Allow the result from a type detection function to be `true` while + * translating that into a string. Old type detection functions will return the + * type name if it passes. An object store would be better, but not backwards + * compatible. * + * @param typeDetect Object or function for type detection + * @param res Result from the type detection function + * @returns Type name or false + */ +function _typeResult(typeDetect, res) { + return res === true ? typeDetect._name : res; +} +/** + * Calculate the 'type' of a column * @param settings DataTables settings object - * @param rowIdx Row to consider - * @param trIn TR element to add to the table - optional. If not given, - * DataTables will create a row automatically - * @param tds Array of TD|TH elements for the row - must be given if trIn is. */ -function createTr(settings, rowIdx, trIn, tds) { - var row = settings.data[rowIdx], cells = [], tr, td, column, i, iLen, create, trClass = settings.classes.tbody.row; - if (row && row.tr === null) { - let rowData = row.data; - tr = trIn || document.createElement('tr'); - row.tr = tr; - row.cells = cells; - Dom.s(tr).classAdd(trClass); - // Use a private property on the node to allow reserve mapping from the node - // to the aoData array for fast look up - tr._DT_RowIndex = rowIdx; - // Special parameters can be given by the data source to be used on the - // row - rowAttributes(settings, row); - /* Process each column */ - for (i = 0, iLen = settings.columns.length; i < iLen; i++) { - column = settings.columns[i]; - create = trIn && tds && tds[i] ? false : true; - td = create - ? document.createElement(column.cellType) - : tds[i]; - if (!td) { - log(settings, 0, 'Incorrect column count', 18); - } - td._DT_CellIndex = { - row: rowIdx, - column: i - }; - cells.push(td); - var display = getRowDisplay(settings, rowIdx); - // Need to create the HTML if new, or if a rendering function is - // defined - if (create || - ((column.render || column.data !== i) && - (!util.is.plainObject(column.data) || - (column.data && - column.data._ !== i + '.display')))) { - writeCell(td, display[i]); - } - // column class - Dom.s(td).classAdd(column.className); - // Visibility - add or remove as required - if (column.visible && create) { - tr.appendChild(td); +function columnTypes(settings, originalTypes = '') { + var columns = settings.columns; + var data = settings.data; + var types = ext.type.detect; + var i, iLen, j, jen, k, ken; + var col, detectedType, cache; + if (!originalTypes) { + originalTypes = columns.map(c => c.type).join(','); + } + // For each column, spin over the data type detection functions, seeing if + // one matches + for (i = 0, iLen = columns.length; i < iLen; i++) { + col = columns[i]; + cache = []; + if (!col.type && col.typeManual) { + col.type = col.typeManual; + } + else if (!col.type) { + // With SSP type detection can be unreliable and error prone, so we + // provide a way to turn it off. + if (!settings.typeDetect) { + return; } - else if (!column.visible && !create) { - td.parentNode.removeChild(td); + for (j = 0, jen = types.length; j < jen; j++) { + let typeDetect = types[j]; + let oneOf; + let allOf; + let init; + let one = false; + // There can be either one, or three type detection functions + if (typeof typeDetect === 'function') { + allOf = typeDetect; + } + else { + oneOf = typeDetect.oneOf; + allOf = typeDetect.allOf; + init = typeDetect.init; + } + detectedType = null; + // Fast detect based on column assignment + if (init) { + detectedType = _typeResult(typeDetect, init(settings, col, i)); + if (detectedType) { + col.type = detectedType; + break; + } + } + for (k = 0, ken = data.length; k < ken; k++) { + if (!data[k]) { + continue; + } + // Use a cache array so we only need to get the type data + // from the formatter once (when using multiple detectors) + if (cache[k] === undefined) { + cache[k] = getCellData(settings, k, i, 'type'); + } + // Only one data point in the column needs to match this + // function + if (oneOf && !one) { + one = _typeResult(typeDetect, oneOf(cache[k], settings)); + } + // All data points need to match this function + detectedType = _typeResult(typeDetect, allOf(cache[k], settings)); + // If null, then this type can't apply to this column, so + // rather than testing all cells, break out. There is an + // exception for the last type which is `html`. We need to + // scan all rows since it is possible to mix string and HTML + // types + if (!detectedType && j !== types.length - 3) { + break; + } + // Only a single match is needed for html type since it is + // bottom of the pile and very similar to string - but it + // must not be empty + if (detectedType === 'html' && !util.is.empty(cache[k])) { + break; + } + } + // Type is valid for all data points in the column - use this + // type + if ((oneOf && one && detectedType) || + (!oneOf && detectedType)) { + col.type = detectedType; + break; + } } - if (column.createdCell) { - column.createdCell.call(settings.instance, td, getCellData(settings, rowIdx, i), rowData, rowIdx, i); + // Fall back - if no type was detected, always use string + if (!col.type) { + col.type = 'string'; } } - callbackFire(settings, 'rowCreated', 'row-created', [ - tr, - rowData, - rowIdx, - cells - ]); - } - else if (row) { - Dom.s(row.tr).classAdd(trClass); + // Set class names for header / footer for auto type classes + var autoClass = ext.type.className[col.type]; + if (autoClass) { + _columnAutoClass(settings.header, i, autoClass); + _columnAutoClass(settings.footer, i, autoClass); + } + var renderer = ext.type.render[col.type]; + // This can only happen once! There is no way to remove + // a renderer. After the first time the renderer has + // already been set so createTr will run the renderer itself. + if (renderer && !col.renderer) { + col.renderer = util.get(renderer); + _columnAutoRender(settings, i); + } + } + var newTypes = columns.map(c => c.type).join(','); + if (newTypes !== originalTypes) { + callbackFire(settings, null, 'columnTypes', [settings], false); + } +} +/** + * Apply an auto detected renderer to data which doesn't yet have a renderer + */ +function _columnAutoRender(settings, colIdx) { + let data = settings.data; + for (let i = 0; i < data.length; i++) { + let d = data[i]; + if (d && d.tr) { + // We have to update the display here since there is no invalidation + // check for the data + let display = getCellData(settings, i, colIdx, 'display'); + d.displayData[colIdx] = display; + writeCell(d.cells[colIdx], display); + // No need to update sort / filter data since it has been + // invalidated and will be re-read with the renderer now applied + } } } /** - * Add attributes to a row based on the special `DT_*` parameters in a data - * source object. + * Apply a class name to a column's header cells * - * @param settings DataTables settings object - * @param row Row object for the row to be modified + * @param container The header / footer structure array + * @param colIdx Column index + * @param className Class name to apply */ -function rowAttributes(settings, row) { - var tr = row.tr; - var data = row.data; - if (tr) { - var id = settings.rowIdFn(data); - if (id) { - tr.id = id; - } - if (data.DT_RowClass) { - // Remove any classes added by DT_RowClass before - var a = data.DT_RowClass.split(' '); - row.addedClasses = row.addedClasses - ? util.unique(row.addedClasses.concat(a)) - : a; - Dom.s(tr) - .classRemove(row.addedClasses.join(' ')) - .classAdd(data.DT_RowClass); - } - if (data.DT_RowAttr) { - Dom.s(tr).attr(data.DT_RowAttr); - } - if (data.DT_RowData) { - Dom.s(tr).data(data.DT_RowData); +function _columnAutoClass(container, colIdx, className) { + container.forEach(function (row) { + if (row[colIdx] && row[colIdx].unique) { + Dom.s(row[colIdx].cell).classAdd(className); } - } + }); } /** - * Create the HTML header for the table + * Take the column definitions and static columns arrays and calculate how they + * relate to column indexes. The callback function will then apply the + * definition found for a column to a suitable configuration object. * - * @param settings DataTable instance - * @param side If the header or footer should be used - * @returns + * @param settings DataTables settings object + * @param aoColDefs The aoColumnDefs array that is to be applied + * @param aoCols The aoColumns array that defines columns individually + * @param headerLayout Layout for header as it was loaded + * @param fn Callback function - takes two parameters, the calculated column + * index and the definition for that column. */ -function buildHead(settings, side) { - let classes = settings.classes; - let columns = settings.columns; - let i, iLen, row; - let target = Dom.s(side === 'header' ? settings.thead : settings.tfoot); - let titleProp = side === 'header' ? 'title' : side; - // Footer might be defined - if (!target) { - return; - } - // If no cells yet and we have content for them, then create - if (side === 'header' || - util.array.pluck(settings.columns, titleProp).join('')) { - row = target.find('tr'); - // Add a row if needed - if (!row.count()) { - row = Dom.c('tr').appendTo(target); - } - // Add the number of cells needed to make up to the number of columns - if (row.count() === 1) { - let cellCount = 0; - row.find('td, th').each(el => { - cellCount += el.colSpan; - }); - for (i = cellCount, iLen = columns.length; i < iLen; i++) { - Dom.c('th') - .html(columns[i][titleProp] || '') - .appendTo(row); +function applyColumnDefs(settings, aoColDefs, aoCols, headerLayout, fn) { + var i, iLen, j, jLen, k, kLen; + var columns = settings.columns; + if (aoCols) { + for (i = 0, iLen = aoCols.length; i < iLen; i++) { + // Compat + if (aoCols[i] && aoCols[i].name) { + columns[i].name = aoCols[i].name; } } } - let detected = detectHeader(settings, target.get(0), true); - if (side === 'header') { - settings.header = detected; - target.find('tr').classAdd(classes.thead.row); + // Column definitions with aTargets + if (aoColDefs) { + // Loop over the definitions array - loop in reverse so first instance + // has priority + for (i = aoColDefs.length - 1; i >= 0; i--) { + let def = aoColDefs[i]; + /* Each definition can target multiple columns, as it is an array */ + let aTargets = def.target !== undefined + ? def.target + : def.targets !== undefined + ? def.targets + : def.aTargets; // legacy + if (!Array.isArray(aTargets)) { + aTargets = [aTargets]; + } + for (j = 0, jLen = aTargets.length; j < jLen; j++) { + var target = aTargets[j]; + if (typeof target === 'number' && target >= 0) { + /* Add columns that we don't yet know about */ + while (columns.length <= target) { + addColumn(settings); + } + /* Integer, basic index */ + fn(target, def); + } + else if (typeof target === 'number' && target < 0) { + /* Negative integer, right to left column counting */ + fn(columns.length + target, def); + } + else if (typeof target === 'string') { + for (k = 0, kLen = columns.length; k < kLen; k++) { + if (target === '_all') { + // Apply to all columns + fn(k, def); + } + else if (target.indexOf(':name') !== -1) { + // Column selector + if (columns[k].name === target.replace(':name', '')) { + fn(k, def); + } + } + else { + // Cell selector + headerLayout.forEach(function (row) { + if (row[k]) { + var cell = row[k].cell; + // Legacy support. Note that it means that + // we don't support an element name selector + // only, since they are treated as class + // names for 1.x compat. + if (target.match(/^[a-z][\w-]*$/i)) { + target = '.' + target; + } + if (cell.matches(target)) { + fn(k, def); + } + } + }); + } + } + } + } + } } - else { - settings.footer = detected; - target.find('tr').classAdd(classes.tfoot.row); + // Statically defined columns array + if (aoCols) { + for (i = 0, iLen = aoCols.length; i < iLen; i++) { + fn(i, aoCols[i]); + } } - // Every cell needs to be passed through the renderer - target - .children('tr') - .children('th, td') - .each(el => { - // Should just be able to do `renderer(settings, side)` here but - // Typescript doesn't like it, despite it already being constrained! - let runner = side === 'header' - ? renderer(settings, 'header') - : renderer(settings, 'footer'); - runner(settings, Dom.s(el), classes); - }); } /** - * Build a layout structure for a header or footer + * Get the width for a given set of columns * - * @param settings DataTables settings - * @param source Source layout array - * @param incColumns What columns should be included - * @returns Layout array in column index order + * @param settings DataTables settings object + * @param targets Columns - comma separated string or array of numbers + * @param original Use the original width (true) or calculated (false) + * @param incVisible Include visible columns (true) or not (false) + * @returns Combined CSS value */ -function headerLayout(settings, source, incColumns) { - var row, column, cell; - var local = []; - var structure = []; - var columns = settings.columns; - var columnCount = columns.length; - var rowspan, colspan; - if (!source) { - return; - } - // Default is to work on only visible columns - if (!incColumns) { - incColumns = util.array.range(columnCount).filter(function (idx) { - return columns[idx].visible; - }); - } - // Make a copy of the master layout array, but with only the columns we want - for (row = 0; row < source.length; row++) { - // Remove any columns we haven't selected - local[row] = source[row].slice().filter(function (c, i) { - return incColumns.includes(i); - }); - // Prep the structure array - it needs an element for each row - structure.push([]); +function columnsSumWidth(settings, targets, original, incVisible) { + if (!Array.isArray(targets)) { + targets = columnsFromHeader(targets); } - for (row = 0; row < local.length; row++) { - for (column = 0; column < local[row].length; column++) { - rowspan = 1; - colspan = 1; - // Check to see if there is already a cell (row/colspan) covering - // our target insert point. If there is, then there is nothing to - // do. - if (structure[row][column] === undefined) { - cell = local[row][column].cell; - // Expand for rowspan - while (local[row + rowspan] !== undefined && - local[row][column].cell == local[row + rowspan][column].cell) { - structure[row + rowspan][column] = null; - rowspan++; - } - // And for colspan - while (local[row][column + colspan] !== undefined && - local[row][column].cell == local[row][column + colspan].cell) { - // Which also needs to go over rows - for (var k = 0; k < rowspan; k++) { - structure[row + k][column + colspan] = null; - } - colspan++; - } - var titleSpan = Dom.s(cell).find('.dt-column-title'); - structure[row][column] = { - cell: cell, - colspan: colspan, - rowspan: rowspan, - title: titleSpan.count() - ? titleSpan.html() - : Dom.s(cell).html() - }; + let sum = 0; + let unit = 'px'; + let columns = settings.columns; + for (let i = 0, iLen = targets.length; i < iLen; i++) { + let column = columns[targets[i]]; + let definedWidth = original ? column.widthOrig : column.width; + if (column.visible === false) { + continue; + } + if (definedWidth === null || definedWidth === undefined) { + return null; // can't determine a defined width - browser defined + } + else if (typeof definedWidth === 'number') { + sum += definedWidth; + } + else { + let matched = definedWidth.match(/([\d\.]+)([^\d]*)/); + if (matched) { + sum += parseFloat(matched[1]); + unit = matched.length === 3 ? matched[2] : 'px'; } } } - return structure; + return sum + unit; } /** - * Draw the header (or footer) element based on the column visibility states. + * Determine what columns a header cell covers (can be multiple for colspan + * cases). * - * @param settings DataTables settings object - * @param source Layout array from detectHeader + * @param cell The header cell in question + * @returns An array of column indexes */ -function drawHead(settings, source) { - let layout = headerLayout(settings, source); - let tr; - if (!layout) { - return; +function columnsFromHeader(cell) { + let attr = Dom.s(cell).closest('[data-dt-column]').attr('data-dt-column'); + if (!attr) { + return []; } - for (let row = 0; row < source.length; row++) { - tr = source[row].row; - // All cells are going to be replaced, so empty out the row - if (tr) { - Dom.s(tr).detachChildren(); - } - for (let column = 0; column < layout[row].length; column++) { - let point = layout[row][column]; - if (point) { - Dom.s(point.cell) - .appendTo(tr) - .attr('rowspan', point.rowspan) - .attr('colspan', point.colspan); + return attr.split(',').map(function (val) { + return parseInt(val); + }); +} +/** + * Get cells from the header or footer, including a specific row and / or cell + * + * @param header The header or footer strcture + * @param row A specific row, or null + * @param column A specific column, or null + * @returns An array of all matching cells + */ +function columnCells(header, row = null, column = null) { + var out = []; + for (var i = 0; i < header.length; i++) { + if (row === null || row === i) { + for (var j = 0; j < header[i].length; j++) { + var cell = header[i][j].cell; + if ((column === null || column === j) && !out.includes(cell)) { + out.push(cell); + } } } } + return out; } /** - * Insert the required TR nodes into the table for display + * Get cells that apply for ordering handler or icons * - * @param settings DataTables settings object - * @param ajaxComplete true after ajax call to complete rendering + * @param settings Context + * @param notSelector DOM selector to exclude elements + * @returns Array of selected elements */ -function draw(settings, ajaxComplete) { - // Allow for state saving and a custom start position - setStartPosition(settings); - // Provide a pre-callback function which can be used to cancel the draw is - // false is returned - var aPreDraw = callbackFire(settings, 'preDraw', 'preDraw', [settings]); - if (aPreDraw.indexOf(false) !== -1) { - processingDisplay(settings, false); - return; +function columnOrderingCells(settings, notSelector) { + var cells = []; + var titleRow = settings.titleRow; + if (titleRow === true) { + // Top row (legacy `orderCellsTop`) + cells = columnCells(settings.header, 0); } - var rowEls = []; - var rowCount = 0; - var isServerSide = dataSource(settings) == 'ssp'; - var display = settings.display; - var start = settings.displayStart; - var end = displayEnd(settings); - var columns = settings.columns; - var body = Dom.s(settings.tbody); - settings.doingDraw = true; - /* Server-side processing draw intercept */ - if (settings.deferLoading) { - settings.deferLoading = false; - settings.drawCount++; - processingDisplay(settings, false); + else if (titleRow === false) { + // Bottom row (legacy `orderCellsTop`) + cells = columnCells(settings.header, settings.header.length - 1); } - else if (!isServerSide) { - settings.drawCount++; + else if (titleRow !== null) { + // Specific row + cells = columnCells(settings.header, titleRow); } - else if (!settings.destroying && !ajaxComplete) { - // Show loading message for server-side processing - if (settings.drawCount === 0) { - body.empty().append(_emptyRow(settings)); - } - ajaxUpdate(settings); + else { + // All + cells = columnCells(settings.header); + } + return Dom.s(cells) + .filter('th' + notSelector + ', td' + notSelector) + .filter(el => { + return (Dom.s(el) + .parent() + .filter(notSelector) + .length !== 0); + }); +} + +const footer = (settings, cell, classes) => { + cell.classAdd(classes.tfoot.cell); +}; +const header = (settings, cell, classes) => { + cell.classAdd(classes.thead.cell); + if (!settings.features.ordering) { + cell.classAdd(classes.order.none); + } + // Conditions to not apply the ordering icons + if (!columnOrderingCells(settings, ':not([data-dt-order="disable"])') + .get() + .includes(cell[0])) { return; } - if (display.length !== 0) { - var iStart = isServerSide ? 0 : start; - var iEnd = isServerSide ? settings.data.length : end; - for (var j = iStart; j < iEnd; j++) { - var dataIdx = display[j]; - var data = settings.data[dataIdx]; - // Row has been deleted - can't be displayed - if (data === null) { - continue; - } - // Row node hasn't been created yet - if (data.tr === null) { - createTr(settings, dataIdx); + // No additional mark-up required. Attach a sort listener to update on sort + // - note that using the `DT` namespace will allow the event to be removed + // automatically on destroy, while the `dt` namespaced event is the one we + // are listening for + Dom.s(settings.table).on('order.dt.DT column-visibility.dt.DT', function (e, ctx, column) { + if (settings !== ctx) { + // need to check if this is the host + return; // table, not a nested one + } + var sorting = ctx.sortDetails; + if (!sorting) { + return; + } + var orderedColumns = pluck(sorting, 'col'); + // This handler is only needed on column visibility if the column is + // part of the ordering. If it isn't, then we can bail out to save + // performance. It could be a separate event handler, but this is a + // balance between code reuse / size and performance console.log(e, + // e.name, column, orderedColumns, orderedColumns.includes(column)) + if (e.type === 'column-visibility' && + !orderedColumns.includes(column)) { + return; + } + var i; + var orderClasses = classes.order; + var columns = ctx.api.columns(cell); + var col = settings.columns[columns.flatten()[0]]; + var orderable = columns.orderable().includes(true); + var ariaType = ''; + var indexes = columns.indexes(); + var sortDirs = columns.orderable(true).flatten(); + var tabIndex = settings.tabIndex; + var canOrder = ctx.orderHandler && orderable; + cell.classRemove(orderClasses.isAsc + ' ' + orderClasses.isDesc) + .classToggle(orderClasses.none, !orderable) + .classToggle(orderClasses.canAsc, canOrder && sortDirs.includes('asc')) + .classToggle(orderClasses.canDesc, canOrder && sortDirs.includes('desc')); + // Determine if all of the columns that this cell covers are + // included in the current ordering + var isOrdering = true; + for (i = 0; i < indexes.length; i++) { + if (!orderedColumns.includes(indexes[i])) { + isOrdering = false; } - var nRow = data.tr; - // Add various classes as needed - for (var i = 0; i < columns.length; i++) { - var col = columns[i]; - var td = data.cells[i]; - Dom.s(td) - .classAdd(col.type ? ext.type.className[col.type] : null) // auto class - .classAdd(settings.classes.tbody.cell); // all cells + } + if (isOrdering) { + // Get the ordering direction for the columns under this cell + // Note that it is possible for a cell to be asc and desc + // sorting (column spanning cells) + var orderDirs = columns.order(); + cell.classAdd((orderDirs.includes('asc') ? orderClasses.isAsc : '') + + (orderDirs.includes('desc') ? orderClasses.isDesc : '')); + } + // Find the first visible column that has ordering applied to it - + // it get's the aria information, as the ARIA spec says that only + // one column should be marked with aria-sort + var firstVis = -1; // column index + for (i = 0; i < orderedColumns.length; i++) { + if (settings.columns[orderedColumns[i]].visible) { + firstVis = orderedColumns[i]; + break; } - // Row callback functions - might want to manipulate the row - // rowCount and j are not currently documented. Are they at all - // useful? - callbackFire(settings, 'row', null, [ - nRow, - data.data, - rowCount, - j, - dataIdx - ]); - rowEls.push(nRow); - rowCount++; } - } - else { - rowEls[0] = _emptyRow(settings); - } - /* Header and footer callbacks */ - callbackFire(settings, 'header', 'header', [ - Dom.s(settings.thead).children('tr').get(0), - getDataMaster(settings), - start, - end, - display - ]); - callbackFire(settings, 'footer', 'footer', [ - Dom.s(settings.tfoot).children('tr').get(0), - getDataMaster(settings), - start, - end, - display - ]); - body.detachChildren().append(rowEls); - // Empty table needs a specific class - Dom.s(settings.tableWrapper).classToggle('dt-empty-footer', Dom.s(settings.tfoot).find('tr').count() === 0); - // Call all required callback functions for the end of a draw - callbackFire(settings, 'draw', 'draw', [settings], true); - // Draw is complete, sorting and filtering must be as well - settings.wasOrdered = false; - settings.wasFiltered = false; - settings.doingDraw = false; -} -/** - * Redraw the table - taking account of the various features which are enabled - * - * @param settings DataTables settings object - * @param holdPosition Keep the current paging position. By default the paging - * is reset to the first page - * @param recompute Indicate if a rebuild of sort and filter should happen - */ -function reDraw(settings, holdPosition, recompute) { - let features = settings.features, doSort = features.ordering, doFilter = features.searching; - if (recompute === undefined || recompute === true) { - // Resolve any column types that are unknown due to addition or - // invalidation - columnTypes(settings); - columnWidths(settings); - if (doSort) { - sort(settings); + if (indexes[0] == firstVis) { + var firstSort = sorting[0]; + var sortOrder = col.orderSequence; + cell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending'); + // Determine if the next click will remove sorting or change the + // sort + ariaType = + sortOrder && !sortOrder[firstSort.index + 1] + ? 'Remove' + : 'Reverse'; } - if (doFilter) { - filterComplete(settings); + else { + cell.attrRemove('aria-sort'); + } + // Make the headers tab-able for keyboard navigation + if (orderable) { + var orderSpan = cell.find('.dt-column-order'); + orderSpan + .attr('role', 'button') + .attr('aria-label', orderable + ? col.ariaTitle + + ctx.api.i18n('aria.orderable' + ariaType) + : col.ariaTitle); + if (tabIndex !== -1) { + orderSpan.attr('tabindex', tabIndex); + } + } + }); +}; +const layout = (settings, container, items) => { + let classes = settings.classes.layout; + let row = Dom.c('div') + .attr('id', items.id || null) + .classAdd(items.className || classes.row) + .appendTo(container); + displayRowCells(items, function (key, val) { + var klass = ''; + if (val.table) { + row.classAdd(classes.tableRow); + klass += classes.tableCell + ' '; + } + if (key === 'start') { + klass += classes.start; + } + else if (key === 'end') { + klass += classes.end; } else { - // No filtering, so we want to just use the display master - settings.display = settings.displayMaster.slice(); + klass += classes.full; } - } - if (holdPosition !== true) { - settings.displayStart = 0; - } - else { - // Keep position, but make sure that there is actually data to display, - // otherwise we need to rewind a bit (e.g. if rows were deleted) - lengthOverflow(settings); - } - // Let any modules know about the draw hold position state (used by - // scrolling internally) - settings.drawHold = holdPosition; - draw(settings); - settings.api.one('draw', function () { - settings.drawHold = false; + Dom.c('div') + .attr({ + id: val.id || null, + class: val.className + ? val.className + : classes.cell + ' ' + klass + }) + .append(val.contents) + .appendTo(row); }); -} -/** - * Table is empty - create a row with an empty message in it - * - * @param settings DataTables context - */ -function _emptyRow(settings) { - let lang = settings.language; - let zero = lang.zeroRecords; - let dataSrc = dataSource(settings); - // Make use of the fact that settings.json is only set once the initial data - // has been loaded. Show loading when that isn't the case - if ((dataSrc === 'ssp' || dataSrc === 'ajax') && !settings.json) { - zero = lang.loadingRecords; - } - else if (lang.emptyTable && recordsTotal(settings) === 0) { - zero = lang.emptyTable; +}; +const pagingButton = (settings, buttonType, content, active, disabled) => { + var classes = settings.classes.paging; + var btnClasses = [classes.button]; + var btn; + if (active) { + btnClasses.push(classes.active); } - return Dom - .c('tr') - .append(Dom - .c('td') - .attr('colSpan', visibleColumns(settings)) - .classAdd(settings.classes.empty.row) - .html(zero)) - .get(0); -} -/** - * Use the DOM source to create up an array of header cells. The idea here is to - * create a layout grid (array) of rows x columns, which contains a reference to - * the cell at that point in the grid (regardless of col/rowspan), such that any - * column / row could be removed and the new grid constructed. - * - * @param settings DataTables context - * @param thead thead / tbody element - * @param write If cells should be written (if required) - * @returns Calculated layout array - */ -function detectHeader(settings, thead, write) { - let columns = settings.columns; - let rows = Dom.s(thead).children('tr'); - let row, loopCell; - let i, k, l, len, shifted, column, colspan, rowspan; - let titleRow = settings.titleRow; - let isHeader = thead && thead.nodeName.toLowerCase() === 'thead'; - let layout = []; - let isUnique; - let shift = function (a, b, j) { - let d = a[b]; - while (d[j]) { - j++; - } - return j; - }; - // We know how many rows there are in the layout - so prep it - for (i = 0, len = rows.count(); i < len; i++) { - layout.push([]); + if (disabled) { + btnClasses.push(classes.disabled); } - for (i = 0, len = rows.count(); i < len; i++) { - row = rows.get(i); - column = 0; - // For every cell in the row.. - loopCell = row.firstChild; - while (loopCell) { - if (loopCell.nodeName.toUpperCase() == 'TD' || - loopCell.nodeName.toUpperCase() == 'TH') { - let cell = Dom.s(loopCell); - let cols = []; - // Get the col and rowspan attributes from the DOM and sanitise - // them - colspan = parseInt(cell.attr('colspan') || '1') || 1; - rowspan = parseInt(cell.attr('rowspan') || '1') || 1; - colspan = - !colspan || colspan === 0 || colspan === 1 ? 1 : colspan; - rowspan = - !rowspan || rowspan === 0 || rowspan === 1 ? 1 : rowspan; - // There might be colspan cells already in this row, so shift - // our target accordingly - shifted = shift(layout, i, column); - // Cache calculation for unique columns - isUnique = colspan === 1 ? true : false; - // Perform header setup - if (write) { - if (isUnique) { - // Allow column options to be set from HTML attributes - columnOptions(settings, shifted, escapeObject(cell.data())); - // Get the width for the column. This can be defined - // from the width attribute, style attribute or - // `columns.width` option - let columnDef = columns[shifted]; - let width = cell.attr('width') || null; - let t = cell - .get(0) - .style.width.match(/width:\s*(\d+[pxem%]+)/); - if (t) { - width = t[1]; - } - columnDef.widthOrig = columnDef.width || width; - if (isHeader) { - // Column title handling - can be user set, or read - // from the DOM This happens before the render, so - // the original is still in place - if (columnDef.title !== null && - !columnDef.autoTitle) { - if ((titleRow === true && i === 0) || // top row - (titleRow === false && - i === rows.count() - 1) || // bottom row - titleRow === i || // specific row - titleRow === null) { - cell.html(columnDef.title); - } - } - if (!columnDef.title && isUnique) { - columnDef.title = util.string.stripHtml(cell.html()); - columnDef.autoTitle = true; - } - } - else { - // Footer specific operations - if (columnDef.footer) { - cell.html(columnDef.footer); - } - } - // Fall back to the aria-label attribute on the table - // header if no ariaTitle is provided. - if (!columnDef.ariaTitle) { - columnDef.ariaTitle = - cell.attr('aria-label') || columnDef.title; - } - // Column specific class names - if (columnDef.className) { - cell.classAdd(columnDef.className); - } - } - // Wrap the column title so we can write to it in future - if (cell.find('div.dt-column-title').count() === 0) { - Dom.c('div') - .classAdd('dt-column-title') - .append(Array.from(cell.get(0).childNodes)) - .appendTo(cell); - } - if (settings.orderIndicators && - isHeader && - cell.filter(':not([data-dt-order=disable])').count() !== - 0 && - cell.parent(':not([data-dt-order=disable])').count() !== - 0 && - cell.find('div.dt-column-order').count() === 0) { - Dom.c('div') - .classAdd('dt-column-order') - .appendTo(cell); - } - // We need to wrap the elements in the header in another - // element to use flexbox layout for those elements - var headerFooter = isHeader ? 'header' : 'footer'; - if (cell.find('div.dt-column-' + headerFooter).count() === - 0) { - Dom.c('div') - .classAdd('dt-column-' + headerFooter) - .append(Array.from(cell.get(0).childNodes)) - .appendTo(cell); - } - } - // If there is col / rowspan, copy the information into the - // layout grid - for (l = 0; l < colspan; l++) { - for (k = 0; k < rowspan; k++) { - layout[i + k][shifted + l] = { - cell: cell.get(0), - unique: isUnique - }; - layout[i + k].row = row; - } - cols.push(shifted + l); - } - // Assign an attribute so spanning cells can still be identified - // as belonging to a column - cell.attr('data-dt-column', util.unique(cols).join(',')); - } - loopCell = loopCell.nextSibling; - } + if (buttonType === 'ellipsis') { + btn = Dom.c('span').classAdd('ellipsis').html(content).get(0); } - return layout; -} -/** - * Set the start position for draw - * - * @param settings DataTables settings object - */ -function setStartPosition(settings) { - var bServerSide = dataSource(settings) == 'ssp'; - var iInitDisplayStart = settings.displayStartInit; - // Check and see if we have an initial draw position from state saving - if (iInitDisplayStart !== undefined && iInitDisplayStart !== -1) { - settings.displayStart = bServerSide - ? iInitDisplayStart - : iInitDisplayStart >= recordsDisplay(settings) - ? 0 - : iInitDisplayStart; - settings.displayStartInit = -1; + else { + btn = Dom.c('button') + .classAdd(btnClasses.join(' ')) + .attr('role', 'link') + .attr('type', 'button') + .html(content) + .get(0); + } + return { + display: btn, + clicker: btn + }; +}; +const pagingContainer = (settings, buttons) => { + // No wrapping element - just append directly to the host + return buttons; +}; +function displayRowCells(items, fn) { + if (items.start) { + fn('start', items.start); + } + if (items.end) { + fn('end', items.end); + } + if (items.full) { + fn('full', items.full); } } + /** - * Get the number of records in the current record set, before filtering - * - * @param ctx DataTables settings object - */ -function recordsTotal(ctx) { - return dataSource(ctx) == 'ssp' - ? ctx.recordsTotal * 1 - : ctx.displayMaster.length; -} -/** - * Get the number of records in the current record set, after filtering + * DataTables extensions * - * @param ctx DataTables settings object - */ -function recordsDisplay(ctx) { - return dataSource(ctx) == 'ssp' - ? ctx.recordsDisplay * 1 - : ctx.display.length; -} -/** - * Get the display end point - display index + * This namespace acts as a collection area for plug-ins that can be used to + * extend DataTables capabilities. Indeed many of the build in methods + * use this method to provide their own capabilities (sorting methods for + * example). * - * @param ctx DataTables settings object + * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy + * reasons */ -function displayEnd(ctx) { - var len = ctx.pageLength, start = ctx.displayStart, calc = start + len, records = ctx.display.length, features = ctx.features, paginate = features.paging; - if (features.serverSide) { - return paginate === false || len === -1 - ? start + records - : Math.min(start + len, ctx.recordsDisplay); - } - else { - return !paginate || calc > records || len === -1 ? records : calc; - } -} +const ext = { + /** + * DataTables build type (expanded by the download builder) + */ + builder: 'bs5/dt-3.0.4', + /** + * Buttons. For use with the Buttons extension for DataTables. This is + * defined here so other extensions can define buttons regardless of load + * order. It is _not_ used by DataTables core. + */ + buttons: {}, + /** + * ColumnControl buttons and content + */ + ccContent: {}, + /** + * Element class names + */ + classes: classes$1, + /** + * Error reporting. + * + * How should DataTables report an error. Can take the value 'alert', + * 'throw', 'none' or a function. + */ + errMode: 'alert', + /** HTML entity escaping */ + escape: { + /** When reading data-* attributes for initialisation options */ + attributes: false + }, + /** + * Legacy so v1 plug-ins don't throw js errors on load + */ + feature: legacy, + /** + * Feature plug-ins. + * + * This is an object of callbacks which provide the features for DataTables + * to be initialised via the `layout` option. + */ + features: features, + /** + * Row searching. + * + * This method of searching is complimentary to the default type based + * searching, and a lot more comprehensive as it allows you complete control + * over the searching logic. Each element in this array is a function + * (parameters described below) that is called for every row in the table, + * and your logic decides if it should be included in the searching data set + * or not. + */ + search: [], + /** + * Selector extensions + * + * The `selector` option can be used to extend the options available for the + * selector modifier options (`selector-modifier` object data type) that + * each of the three built in selector types offer (row, column and cell + + * their plural counterparts). For example the Select extension uses this + * mechanism to provide an option to select only rows, columns and cells + * that have been marked as selected by the end user (`{selected: true}`), + * which can be used in conjunction with the existing built in selector + * options. + */ + selector: { + cell: [], + column: [], + row: [] + }, + settings: [], + /** + * Legacy configuration options. Enable and disable legacy options that + * are available in DataTables. + * + * @type object + */ + legacy: { + /** + * Enable / disable DataTables 1.9 compatible server-side processing + * requests + */ + ajax: null + }, + /** + * Pagination plug-in methods. + * + * Each entry in this object is a function and defines which buttons should + * be shown by the pagination rendering method that is used for the table. + * The renderer addresses how the buttons are displayed in the document, + * while the functions here tell it what buttons to display. This is done by + * returning an array of button descriptions (what each button will do). + */ + pager: pager, + renderer: { + footer: { + _: footer + }, + header: { + _: header + }, + layout: { + _: layout + }, + pagingButton: { + _: pagingButton + }, + pagingContainer: { + _: pagingContainer + } + }, + /** + * Rendering helper function exposed for use by the styling integrations. + */ + rendererDisplayRowCells: displayRowCells, + /** + * Ordering plug-ins - custom data source + * + * The extension options for ordering of data available here is + * complimentary to the default type based ordering that DataTables + * typically uses. It allows much greater control over the data that is + * being used to order a column, but is necessarily therefore more complex. + */ + order: {}, + /** + * Type based plug-ins. + * + * Each column in DataTables has a type assigned to it, either by automatic + * detection or by direct assignment using the `type` option for the column. + * The type of a column will effect how it is ordering and search (plug-ins + * can also make use of the column type if required). + */ + type: store, + /** + * Unique DataTables instance counter + * + * @type int + * @private + */ + _unique: 0, + // + // Depreciated + // The following properties are retained for backwards compatibility only. + // The should not be used in new projects and will be removed in a future + // version + // + /** + * Software version + * @type string + */ + version: '3.0.4' +}; +// +// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts +// +Object.assign(ext, { + afnFiltering: ext.search, + aTypes: ext.type.detect, + ofnSearch: ext.type.search, + oSort: ext.type.order, + afnSortData: ext.order, + aoFeatures: ext.feature, + oStdClasses: ext.classes, + oPagination: ext.pager, + sVersion: ext.version, + fnVersionCheck: check$1 +}); /** * Common run function for selector types @@ -9863,10 +9893,7 @@ function columnHeader(settings, column, row) { // backwards compatibility) for (var i = 0; i < header.length; i++) { if (header[i][column].unique && - Dom - .s(header[i][column].cell) - .find('.dt-column-title') - .text()) { + Dom.s(header[i][column].cell).find('.dt-column-title').text()) { target = i; } } @@ -9876,20 +9903,8 @@ function columnHeader(settings, column, row) { } return header[target][column].cell; } -function columnHeaderCells(header) { - var out = []; - for (var i = 0; i < header.length; i++) { - for (var j = 0; j < header[i].length; j++) { - var cell = header[i][j].cell; - if (!out.includes(cell)) { - out.push(cell); - } - } - } - return out; -} function selectColumns(settings, selector, opts) { - var columns = settings.columns, names, titles, nodes = columnHeaderCells(settings.header); + var columns = settings.columns, names, titles; var run = function (s) { var selInt = intVal(s); // Selector - all @@ -9943,8 +9958,8 @@ function selectColumns(settings, selector, opts) { } // Selector if (match && match[1]) { - return Dom - .s(nodes[mapIdx]) + let columnElements = columnCells(settings.header, null, col.idx); + return Dom.s(columnElements) .filter(match[1]) .count() > 0 ? mapIdx @@ -9980,11 +9995,10 @@ function selectColumns(settings, selector, opts) { return [s._DT_CellIndex.column]; } // Selector on the TH elements for the columns - var result = Dom - .s(nodes) + var result = Dom.s(columnCells(settings.header)) .filter(s) .mapTo(el => { - return columnsFromHeader(el); // `nodes` is column index complete and in order + return columnsFromHeader(el); }) .flat() .sort(function (a, b) { @@ -10121,9 +10135,7 @@ registerPlural('columns().titles()', 'column().title()', function (title, row) { row = title; title = undefined; } - var span = Dom - .s(this.column(column).header(row)) - .find('.dt-column-title'); + var span = Dom.s(this.column(column).header(row)).find('.dt-column-title'); if (title !== undefined) { span.html(title); return this; @@ -10194,9 +10206,7 @@ registerPlural('columns().widths()', 'column().width()', function () { // be read, regardless of colspan in the header and rows being present // in the body var columns = this.columns(':visible'); - var row = Dom - .c('tr') - .html('' + Array(columns.count()).join('') + ''); + var row = Dom.c('tr').html('' + Array(columns.count()).join('') + ''); Dom.s(this.table().body()).append(row); var widths = []; var indexes = columns.indexes(); @@ -10907,7 +10917,7 @@ register('search()', function (input, regex, smart, caseInsen) { } let target = ctx.searches['*']; if (!target) { - target = create$2(); + target = create$1(); } if (typeof regex === 'object') { // New style object of options @@ -10942,7 +10952,7 @@ register('search.fixed()', function (name, search, options) { else { let target = fixed[name]; if (!target || !util.is.plainObject(target)) { - target = create$2(); + target = create$1(); } if (options) { assign(target, options); @@ -10966,7 +10976,7 @@ register(['columns().search()', 'column().search()'], function (input, regex, sm let colIdxs = columns.join(','); let target = ctx.searches[colIdxs]; if (!target) { - target = create$2(); + target = create$1(); } // Delete the search for custom grouping types if removing if ((input === '' || input === null) && columns.length > 1) { @@ -11026,7 +11036,7 @@ register(['columns().search.fixed()', 'column().search.fixed()'], function (name else { let target = fixed[name]; if (!target || !util.is.plainObject(target)) { - target = create$2(); + target = create$1(); } if (options) { assign(target, options); @@ -11958,7 +11968,7 @@ register$2('search', function (settings, optsIn) { let searchName = opts.columns === '*' ? '*' : indexes.join(','); let appliedSearch = settings.searches[searchName]; if (!appliedSearch) { - appliedSearch = create$2(); + appliedSearch = create$1(); settings.searches[searchName] = appliedSearch; } appliedSearch.columns = indexes; @@ -12206,8 +12216,8 @@ function create(parts = {}) { var models = { Column: Settings, - Row: create$1, - Search: create$2, + Row: create$2, + Search: create$1, Settings: create }; @@ -12220,7 +12230,7 @@ const defaults = { autoWidth: true, caption: '', classes: {}, - column: defaults$4, + column: defaults$2, columnDefs: null, columns: null, createdRow: null, @@ -12392,8 +12402,8 @@ const DataTable = function (selector, options) { } table.trigger('options.dt', true, [init]); // Backwards compatibility parameter mapping - compatOpts(defaults); - compatCols(defaults$4); + compatOpts(defaults, true); + compatCols(defaults$2); // Allow data properties on the table element to be used as // initialisation options util.object.assign(init, escapeObject(table.data())); @@ -12522,7 +12532,7 @@ const DataTable = function (selector, options) { ]); map(settings.language, config, 'infoCallback'); // Setup global search - settings.searches['*'] = create$2(config.search); + settings.searches['*'] = create$1(config.search); /* Callback functions which are array driven */ callbackReg(settings, 'draw', config.drawCallback); callbackReg(settings, 'stateSaveParams', config.stateSaveParams); From 3347698712d3e99e652ad4394ef2fc0ce2800fdd Mon Sep 17 00:00:00 2001 From: "Berk D. Demir" <11135+bdd@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:34:41 -0700 Subject: [PATCH 19/20] Add `pm-34171-card-scanner` feature flag (#7477) Introduced in 2026.4.1 Mobile releases (Android and iOS), enables use of device camera to autofill a card type item. N.B. This feature relies on Google ML Kit. F-Droid policy disallows Google ML Kit. Because of this, F-Droid builds do not ship with card scanner feature. (See https://github.com/bitwarden/android/pull/6890) --- .env.template | 1 + src/config.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index 8ed6c5c2..62231776 100644 --- a/.env.template +++ b/.env.template @@ -403,6 +403,7 @@ ## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2) ## - "pm-30529-webauthn-related-origins": ## - "pm-32009-new-item-types": Enable new item types: Bank Account, Driver's License, and Passport (Clients >= 2026.4.0) +## - "pm-34171-card-scanner": Enable the new card scanner feature on mobile (Android >= 2026.4.1, iOS >= 2026.4.1) ## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) diff --git a/src/config.rs b/src/config.rs index 0bb22f7f..9f0ae2e1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1440,6 +1440,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "mutual-tls", "cxp-import-mobile", "cxp-export-mobile", + "pm-34171-card-scanner", // Platform Team "pm-30529-webauthn-related-origins", // Vault Team From cc67d644f62605cb46f4d16c4a2eed1a861cc8bb Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Fri, 18 Sep 2026 20:35:11 +0200 Subject: [PATCH 20/20] set user_created bool for each separate invitation (#7753) --- src/api/core/organizations.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 4f490854..36297d30 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1075,7 +1075,7 @@ async fn send_invite( && data.permissions.get("deleteAnyCollection") == Some(&json!(true)) && data.permissions.get("createNewCollections") == Some(&json!(true))); - let mut user_created: bool = false; + let mut user_created: bool; for email in &data.emails { let mut member_status = MembershipStatus::Invited as i32; let user = match User::find_by_mail(email, &conn).await { @@ -1110,6 +1110,7 @@ async fn send_invite( member_status = MembershipStatus::Accepted as i32; } } + user_created = false; user } };