Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 44 additions & 23 deletions src/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ use anyhow::{Context as _, Result, bail};
use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
use tokio::sync::RwLock;

use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::chat::{ChatId, add_device_msg};
use crate::config::Config;
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
use crate::ephemeral::start_ephemeral_timers;
use crate::imex::{self, BLOBS_BACKUP_NAME};
use crate::location;
use crate::log::{LogExt, warn};
use crate::message::MsgId;
use crate::net::dns::prune_dns_cache;
use crate::net::http::http_cache_cleanup;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
use crate::tools::{SystemTime, delete_file, time};
use crate::{location, stock_str};

/// Extension to [`rusqlite::ToSql`] trait
/// which also includes [`Send`] and [`Sync`].
Expand Down Expand Up @@ -214,32 +213,17 @@ impl Sql {
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.

let recode_avatar = migrations::run(context, self)
let show_unencrypted_device_msg = migrations::run(context, self)
.await
.context("failed to run migrations")?;

// (2) updates that require high-level objects
// the structure is complete now and all objects are usable

if recode_avatar && let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
match blob.recode_to_avatar_size(context).await {
Ok(()) => {
if let Some(path) = blob.to_abs_path().to_str() {
context
.set_config_internal(Config::Selfavatar, Some(path))
.await?;
} else {
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
}
}
Err(e) => {
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
context
.set_config_internal(Config::Selfavatar, None)
.await?
}
}
if show_unencrypted_device_msg {
let txt = stock_str::no_more_unencrypted(context);
let mut msg = crate::message::Message::new_text(txt);
add_device_msg(context, Some("unencrypted-device-msg"), Some(&mut msg)).await?;
}

Ok(())
Expand Down Expand Up @@ -684,6 +668,43 @@ impl Sql {
}
}

pub(crate) trait TransactionExt {
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
fn count(&self, query: &str, params: impl rusqlite::Params + Send) -> Result<usize>;

/// Executes a query which is expected to return one row and one
/// column. If the query does not return any rows, returns `Ok(None)`.
fn query_get_value<T>(
&self,
query: &str,
params: impl rusqlite::Params + Send,
) -> Result<Option<T>>
where
T: rusqlite::types::FromSql + Send + 'static;
}

impl TransactionExt for rusqlite::Transaction<'_> {
fn count(&self, query: &str, params: impl rusqlite::Params + Send) -> Result<usize> {
let count: isize = self.query_row(query, params, |row| row.get(0))?;
Ok(usize::try_from(count)?)
}

fn query_get_value<T>(
&self,
query: &str,
params: impl rusqlite::Params + Send,
) -> Result<Option<T>>
where
T: rusqlite::types::FromSql + Send + 'static,
{
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Ok(Some(res)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(err.into()),
}
}
}

/// Creates a new SQLite connection.
///
/// `path` is the database path.
Expand Down
125 changes: 122 additions & 3 deletions src/sql/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::key::DcKey;
use crate::log::warn;

use crate::sql::Sql;
use crate::sql::TransactionExt as _;
use crate::tools::{self, Time, inc_and_check, time_elapsed};
use crate::transport::ConfiguredLoginParam;

Expand Down Expand Up @@ -713,6 +714,111 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
Ok(())
}

fn unencrypted_chats_migration(
context: &Context,
transaction: &mut rusqlite::Transaction<'_>,
) -> Result<bool> {
let mut show_unencrypted_device_msg = false;

// Migrate:
// - all unencrypted (i.e. ad-hoc) groups to have 0 members
// - all chats of type Mailinglist into a group with 0 members
transaction.execute_batch(
"
CREATE TEMP TABLE temp.legacy_unencrypted_chats(chat_id INTEGER PRIMARY KEY, type INTEGER) STRICT;

INSERT INTO temp.legacy_unencrypted_chats(chat_id, type)
SELECT id, type FROM chats
WHERE ((type=120 AND grpid='') OR type=140) -- Ad-hoc groups and mailinglists
AND id>9;

DELETE FROM chats_contacts
WHERE chat_id IN (SELECT chat_id FROM temp.legacy_unencrypted_chats);

UPDATE chats SET type=120
WHERE id IN (SELECT chat_id FROM temp.legacy_unencrypted_chats);
",
)?;

// Rewrite all address-contacts to have "Hidden" origin.
// We still need the contacts because we want to keep the messages, and every message needs a sender.
// Make sure that the address is available in the name, so that the user can still see it.
transaction.execute_batch(
"
UPDATE contacts
SET origin=8 -- Origin::Hidden
WHERE fingerprint='' AND id>9;

UPDATE contacts
SET name=name || ' (' || addr || ')'
WHERE fingerprint='' AND id>9 AND name!='';

UPDATE contacts
SET authname=authname || ' (' || addr || ')'
WHERE fingerprint='' AND id>9 AND name='' AND authname!='';

UPDATE contacts
SET authname=addr
WHERE fingerprint='' AND id>9 AND name='' AND authname='';

-- Also update the names of the chats:
UPDATE chats
SET name = (
SELECT CASE
WHEN c.name != '' THEN c.name
WHEN c.authname != '' THEN c.authname
ELSE c.addr
END
FROM chats_contacts cc
JOIN contacts c ON c.id = cc.contact_id
WHERE cc.chat_id = chats.id
)
WHERE type = 100 AND id > 9
AND EXISTS (
SELECT 1 FROM chats_contacts cc
JOIN contacts c ON c.id = cc.contact_id
WHERE cc.chat_id = chats.id AND c.fingerprint = '' AND c.id > 9
);
",
)?;

let legacy_chats =
transaction.count("SELECT COUNT(*) FROM temp.legacy_unencrypted_chats", ())?;
let legacy_contacts = transaction.count(
"SELECT COUNT(*) FROM contacts WHERE fingerprint='' AND id>9",
(),
)?;
if legacy_chats > 0 || legacy_contacts > 0 {
// Set the gray letter avatar for all legacy chats:
let blob = crate::blob::BlobObject::create_and_deduplicate_from_bytes(
context,
include_bytes!("../../assets/icon-unencrypted.png"),
"icon-unencrypted.png",
)?;
let new_param = &format!("i={}", blob.as_name());
transaction.execute(
"UPDATE chats SET param=? WHERE id IN (SELECT chat_id FROM temp.legacy_unencrypted_chats)",
(new_param,),
)?;
transaction.execute(
"UPDATE contacts SET param=? WHERE fingerprint='' AND id>9",
(new_param,),
)?;

let force_encryption: Option<String> = transaction.query_get_value(
"SELECT value FROM config WHERE keyname='force_encryption'",
(),
)?;
if force_encryption == Some("0".to_string()) {
show_unencrypted_device_msg = true;
}
}

transaction.execute("DROP TABLE temp.legacy_unencrypted_chats", ())?;

Ok(show_unencrypted_device_msg)
}

impl Sql {
async fn set_db_version(&self, version: i32) -> Result<()> {
self.set_raw_config_int(VERSION_CFG, version).await?;
Expand Down Expand Up @@ -812,7 +918,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
}

let dbversion = dbversion_before_update;
let mut recode_avatar = false;
let mut show_unencrypted_device_msg = false;

if dbversion < 1 {
sql.execute_migration(
Expand Down Expand Up @@ -1187,7 +1293,7 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
.await?;
}
if dbversion < 77 {
recode_avatar = true;
// removed
sql.set_db_version(77).await?;
}
if dbversion < 78 {
Expand Down Expand Up @@ -2687,6 +2793,19 @@ CREATE TABLE smtp_success (
.await?;
}

inc_and_check(&mut migration_version, 169)?;
if dbversion < migration_version {
sql.execute_migration_transaction(
|transaction| {
show_unencrypted_device_msg = unencrypted_chats_migration(context, transaction)?;

Ok(())
},
migration_version,
)
.await?;
}

let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
Expand All @@ -2701,7 +2820,7 @@ CREATE TABLE smtp_success (
}
info!(context, "Database version: v{new_version}.");

Ok(recode_avatar)
Ok(show_unencrypted_device_msg)
}

#[cfg(test)]
Expand Down
Loading