#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - bin/update-roundcube-db Copyright 2022 cPanel, L.L.C.
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
## cpdev: installs to /usr/local/cpanel/base/3rdparty/roundcube
package cpanel::bin::update_roundcube;
use strict;
use warnings;
use Cpanel::Config::LoadCpConf ();
use Cpanel::Config::CpConfGuard ();
use Cpanel::Email::RoundCube ();
use Cpanel::Email::RoundCube::DBI ();
use Cpanel::Exception ();
use Cpanel::LoadFile ();
use Cpanel::LoadModule ();
use Cpanel::Debug ();
use Cpanel::MysqlUtils::InnoDB ();
use Cpanel::MysqlUtils::MyCnf::Basic (); ## case 44546
use Cpanel::MysqlUtils::Version ();
use Cpanel::Server::Type ();
exit( script(@ARGV) // 0 ) unless caller();
sub script {
return if _is_dnsonly();
my $daemonize = !grep { $_ eq '--foreground' } @ARGV;
my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf();
my $mysql_version = Cpanel::MysqlUtils::Version::mysqlversion();
## Exit if Roundcube has been converted to SQLite
my $sqlite_version = $0 =~ s/-db/-sqlite-db/r;
if ( exists $cpconf->{'roundcube_db'} && $cpconf->{'roundcube_db'} eq 'sqlite' ) {
print "Roundcube is configured to use SQLite.\n";
print "This update is for Roundcube using MySQL. Running $sqlite_version.\n";
exec $sqlite_version, @ARGV;
die "Failed to exec $sqlite_version";
}
elsif (
!-e '/var/cpanel/i_accept_my_roundcube_may_be_broken_by_1_6' # if you want a probably broken roundcube, here you go
&& (
Cpanel::MysqlUtils::Version::cmp_versions( $mysql_version, '5.6' ) <= 0 # MySQL 5.6 and below do not support the key length
|| Cpanel::MysqlUtils::Version::cmp_versions( $mysql_version, '10.0' ) == 0 # MariaDB 10.0 has the same issue with the key length
)
) {
print "Roundcube 1.6+ does not support the installed version of MySQL/MariaDB ($mysql_version).\n";
print "Your users' Roundcube databases will be converted to SQLite automatically!\n";
_daemonize() if $daemonize;
Cpanel::LoadModule::load_perl_module('Cpanel::SafeRun::Object');
Cpanel::SafeRun::Object->new_or_die( 'program' => '/usr/local/cpanel/scripts/convert_roundcube_mysql2sqlite' );
return 0;
}
_daemonize() if $daemonize;
# None of the args are consulted in here, so don't pass them in
update_roundcube();
if ( exists $cpconf->{'roundcube_db'} && $cpconf->{'roundcube_db'} eq 'mysql_plus_sqlite' ) {
print "Update for Roundcube using MySQL complete. Now running $sqlite_version to update those.\n";
exec $sqlite_version, @ARGV;
die "Failed to exec $sqlite_version";
}
return 0; # for now preserved original behavior and always exit 0
}
sub _is_dnsonly {
return unless Cpanel::Server::Type::is_dnsonly();
Cpanel::Debug::log_warn("bin/update-roundcube-db is disabled on dnsonly server");
return 1;
}
#Named args:
# dbh: optional, a DBD::mysql handle to use
sub update_roundcube {
my (%opts) = @_;
return if _is_dnsonly();
my ( $dbh, $mysql_has_roundcube_db ) = _check_update_roundcube(%opts);
return _do_update_roundcube( %opts, dbh => $dbh, mysql_has_roundcube_db => $mysql_has_roundcube_db );
}
#opts can be:
# dbh - a Cpanel::DBI::Mysql instance (useful for testing/mocking)
sub _check_update_roundcube {
my (%opts) = @_;
my $dbh = $opts{'dbh'};
if ( !$dbh ) {
## note: lifted from updatehorde, which solved a similar problem with $grantHost
## see Fogbugz cases 44546 (roundcube) and 44533 (horde)
my $mysql_user = Cpanel::MysqlUtils::MyCnf::Basic::getmydbuser('root') || 'root';
my $mysql_host = Cpanel::MysqlUtils::MyCnf::Basic::getmydbhost('root') || 'localhost';
my $mysql_port = Cpanel::MysqlUtils::MyCnf::Basic::getmydbport('root') || 3306;
$dbh = Cpanel::Email::RoundCube::DBI::mysql_db_connect( undef, $mysql_host, $mysql_user, Cpanel::MysqlUtils::MyCnf::Basic::getmydbpass('root'), undef, $mysql_port );
if ( !$dbh ) {
Cpanel::Debug::log_die("Failed to connect to MySQL install at $mysql_host");
}
}
my $has_innodb = Cpanel::MysqlUtils::InnoDB::is_enabled_on_dbh($dbh);
## rcube has required InnoDB since 0.2-beta
if ( !$has_innodb ) {
die "Roundcube requires that MySQL support InnoDB tables, but your MySQL installation does not support them.\n";
}
my $dbs_ar = $dbh->selectall_arrayref('SHOW DATABASES');
my $mysql_has_roundcube_db = grep { $_->[0] eq 'roundcube' } @$dbs_ar;
return ( $dbh, $mysql_has_roundcube_db );
}
# Case 117993, deal with partially installed RoundCube on install
sub _attempt_chown_fix_for_roundcube {
Cpanel::Email::RoundCube::lock_roundcube_for_update();
# case 103369, upgrading db "died" and left the updating lock
# in place so roundcube could not run.
# By doing this in an eval, we can report an error
# and remove the locking file.
my $eval_ret = eval { return Cpanel::Email::RoundCube::perform_chown_fix(); };
my $error_string = $@;
# unlock always here
Cpanel::Email::RoundCube::unlock_roundcube_after_update();
if ( !defined $eval_ret ) {
Cpanel::Debug::log_info("Error occurred during chown fix\n$error_string");
return; # modeled after the schema update failure from above
}
else {
Cpanel::Debug::log_info("Roundcube has succeeded doing the chown fix");
}
return $eval_ret;
}
sub _do_update_roundcube {
my (%opts) = @_;
my ( $dbh, $has_db ) = @opts{qw( dbh mysql_has_roundcube_db )};
my $version = Cpanel::Email::RoundCube::get_active_version();
###
Cpanel::Email::RoundCube::init_roundcube_data_dir();
my $version_before_update = Cpanel::Email::RoundCube::get_stored_version();
if ($version_before_update) {
Cpanel::Debug::log_info("Roundcube update from $version_before_update to $version in progress.");
}
else {
Cpanel::Debug::log_info("Roundcube $version install in progress.");
}
Cpanel::Email::RoundCube::lock_roundcube_for_update();
# case 103369, upgrading db "died" and left the updating lock
# in place so roundcube could not run.
# By doing this in an eval, we can report an error
# and remove the locking file.
my $eval_ret = eval {
my $dest_dir = $Cpanel::Email::RoundCube::ROUNDCUBE_DEST_DIR;
##############################
## DATABASE
my $valid_archive;
if ( $ENV{'CPANEL_ROUNDCUBE_INSTALL_VERSION'} ) {
# the RPM will have already archived the database #
$valid_archive = 1;
}
else {
$valid_archive = $has_db && Cpanel::Email::RoundCube::archive_mysql_roundcube($dbh);
}
Cpanel::Email::RoundCube::prune_mysql_roundcube_archives();
my ( $ok, $msg ) = _add_mysql_roundcube_grants($dbh);
return ( 0, $msg ) if !$ok;
my $success;
if ($has_db) {
$success = update_mysql_roundcube_db($dbh);
}
else {
$success = initialize_mysql_roundcube_db($dbh);
}
if ($success) {
Cpanel::Debug::log_info("Schema update to $version was successful");
}
else {
warn $dbh->errstr();
if ($valid_archive) {
print "Restoring previous Roundcube data\n";
$dbh->do('DROP DATABASE roundcube');
_create_mysql_roundcube_db($dbh);
Cpanel::Email::RoundCube::restore_latest_mysql_archive($dbh);
}
return;
}
##############################
my $cpconf_guard = Cpanel::Config::CpConfGuard->new();
if ( $cpconf_guard->{'data'}{'roundcube_db'} && $cpconf_guard->{'data'}{'roundcube_db'} eq 'mysql_plus_sqlite' ) {
Cpanel::Email::RoundCube::generate_hybrid_config( $dest_dir, $dbh );
}
else {
Cpanel::Email::RoundCube::generate_roundcube_config_mysql( $dest_dir, $dbh );
}
Cpanel::Email::RoundCube::write_version_file();
# process any install tasks after the new version is inplace, including schema backed up and updated #
if ( Cpanel::Email::RoundCube::process_custom_roundcube_install_file($dest_dir) ) {
return 1;
}
# Only set it if it isn't already set
if ( !$cpconf_guard->{'data'}{'roundcube_db'} ) {
$cpconf_guard->{'data'}{'roundcube_db'} = 'mysql';
$cpconf_guard->save();
}
return 1;
};
my $error_string = $@;
# unlock always here
Cpanel::Email::RoundCube::unlock_roundcube_after_update();
if ( !defined $eval_ret ) {
Cpanel::Debug::log_info("Error occurred during upgrade\n$error_string");
return; # modeled after the schema update failure from above
}
else {
Cpanel::Debug::log_info("Roundcube updated to $version");
}
return $eval_ret;
}
sub _create_mysql_roundcube_db {
my ($dbh) = @_;
$dbh->do('CREATE DATABASE IF NOT EXISTS roundcube');
my $rc_dbh = $dbh->clone(
{
Username => 'roundcube',
Password => Cpanel::Email::RoundCube::get_roundcube_password(),
mysql_multi_statements => 1,
database => 'roundcube',
}
);
# Will auto-apply initial schema in this case since nothing is in there.
return update_mysql_roundcube_db($dbh);
}
sub _add_mysql_roundcube_grants {
my ($dbh) = @_;
return Cpanel::Email::RoundCube::handle_mysql_roundcube_grants(
'roundcube',
$dbh,
);
}
#Call this to return a $dbh that always points to the `roundcube` db.
sub _ensure_use_of_roundcube_db {
my ($dbh) = @_;
my $db_all = $dbh->selectall_arrayref('SELECT DATABASE()');
if ( !$db_all->[0][0] || ( $db_all->[0][0] ne 'roundcube' ) ) {
$dbh = $dbh->clone();
$dbh->do('USE roundcube');
}
return $dbh;
}
#Call this when MySQL doesn't have the `roundcube` database.
sub initialize_mysql_roundcube_db {
my ($dbh) = @_;
my $success = _create_mysql_roundcube_db($dbh);
$dbh = _ensure_use_of_roundcube_db($dbh);
return $success;
}
#Call this when MySQL already has the `roundcube` database.
sub update_mysql_roundcube_db {
my ($dbh) = @_;
my ($RCUBE_VERSION) = Cpanel::Email::RoundCube::get_version_info();
$dbh = _ensure_use_of_roundcube_db($dbh);
return Cpanel::Email::RoundCube::DBI::ensure_schema_update( $dbh, 'mysql', $RCUBE_VERSION );
}
sub _daemonize {
print STDERR "$0: running in the background.\n";
require Cpanel::Daemonizer::Simple;
Cpanel::Daemonizer::Simple::daemonize();
return;
}
1;