--- loncom/interface/lonmysql.pm	2004/03/03 17:19:06	1.19
+++ loncom/interface/lonmysql.pm	2019/11/20 18:02:55	1.41
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # MySQL utility functions
 #
-# $Id: lonmysql.pm,v 1.19 2004/03/03 17:19:06 matthew Exp $
+# $Id: lonmysql.pm,v 1.41 2019/11/20 18:02:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -31,9 +31,40 @@ package Apache::lonmysql;
 
 use strict;
 use DBI;
-use Apache::lonnet();
 use POSIX qw(strftime mktime);
+use Apache::lonnet;
 
+my $mysqluser;
+my $mysqlpassword;
+my $mysqldatabase;
+my %db_config;
+
+sub set_mysql_user_and_password {
+    # If we are running under Apache and LONCAPA, use the LON-CAPA 
+    # user and password.  Otherwise...? ? ? ?
+    my ($input_mysqluser,$input_mysqlpassword,$input_mysqldatabase) = @_;
+    if (! defined($mysqldatabase)) {
+        $mysqldatabase = 'loncapa';
+    }
+    if (defined($input_mysqldatabase)) {
+        $mysqldatabase = $input_mysqldatabase;
+    }
+    if (! defined($mysqluser) || ! defined($mysqlpassword)) {
+        if (! eval 'require Apache::lonnet();') {
+            $mysqluser = 'www';
+            $mysqlpassword = $Apache::lonnet::perlvar{'lonSqlAccess'};
+        } else {
+            $mysqluser = '';
+            $mysqlpassword = '';
+        }
+    }
+    if (defined($input_mysqluser)) {
+        $mysqluser = $input_mysqluser;
+    } 
+    if (defined($input_mysqlpassword)) {
+        $mysqlpassword = $input_mysqlpassword;
+    }
+}
 
 ######################################################################
 ######################################################################
@@ -56,14 +87,15 @@ and provide a common interface.  The goa
 a complete reimplementation of the DBI interface.  Instead we try to 
 make using mysql as painless as possible.
 
-Each table has a numeric ID that is a parameter to most lonmysql functions.
-The table id is returned by &create_table.  
-If you lose the table id, it is lost forever.
-The table names in MySQL correspond to 
-$ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.$table_id.  If the table id 
-is non-numeric, it is assumed to be the full name of a table.  If you pass
-the table id in a form, you MUST ensure that what you send to lonmysql is
-numeric, otherwise you are opening up all the tables in the MySQL database.
+Each table has a numeric ID that is a parameter to most lonmysql
+functions.  The table id is returned by &create_table.  If you lose
+the table id, it is lost forever.  The table names in MySQL correspond
+to $env{'user.name'}.'_'.$env{'user.domain'}.'_'.$table_id. (With all
+non-word characters removed form user.name and user.domain) If the
+table id is non-numeric, it is assumed to be the full name of a table.
+If you pass the table id in a form, you MUST ensure that what you send
+to lonmysql is numeric, otherwise you are opening up all the tables in
+the MySQL database.
 
 =over 4
 
@@ -330,8 +362,10 @@ connection is established.
 ###############################
 sub connect_to_db { 
     return 1 if ($dbh);
-    if (! ($dbh = DBI->connect("DBI:mysql:loncapa","www",
-                               $Apache::lonnet::perlvar{'lonSqlAccess'},
+    if (! defined($mysqluser) || ! defined($mysqlpassword)) {
+        &set_mysql_user_and_password();
+    }
+    if (! ($dbh = DBI->connect("DBI:mysql:$mysqldatabase",$mysqluser,$mysqlpassword,
                                { RaiseError=>0,PrintError=>0}))) {
         $debugstring = "Unable to connect to loncapa database.";    
         if (! defined($dbh)) {
@@ -343,6 +377,22 @@ sub connect_to_db {
         return undef;
     }
     $debugstring = "Successfully connected to loncapa database.";    
+    # Determine DB configuration
+    undef(%db_config);
+    my $sth = $dbh->prepare("SHOW VARIABLES");
+    $sth->execute();
+    if ($sth->err()) {
+        $debugstring = "Unable to retrieve db config variables";
+        return undef;
+    }
+    foreach my $row (@{$sth->fetchall_arrayref}) {
+        $db_config{$row->[0]} = $row->[1];
+    }
+    #&Apache::lonnet::logthis("MySQL configuration variables");
+    #while (my ($k,$v) = each(%db_config)) {
+    #    &Apache::lonnet::logthis("    '$k' => '$v'");
+    #}
+    #
     return 1;
 }
 
@@ -363,9 +413,12 @@ initialize anything in the lonmysql pack
 
 ###############################
 sub verify_sql_connection {
+    if (! defined($mysqluser) || ! defined($mysqlpassword)) {
+        &set_mysql_user_and_password();
+    }
     my $connection;
-    if (! ($connection = DBI->connect("DBI:mysql:loncapa","www",
-                                      $Apache::lonnet::perlvar{'lonSqlAccess'},
+    if (! ($connection = DBI->connect("DBI:mysql:loncapa",
+                                      $mysqluser,$mysqlpassword,
                                       { RaiseError=>0,PrintError=>0}))) {
         return 0;
     }
@@ -519,10 +572,6 @@ sub update_table_info {
     #
     # Get MySQLs table status information.
     #
-    my @tabledesc = qw/
-        Name Type Row_format Rows Avg_row_length Data_length
-            Max_data_length Index_length Data_free Auto_increment 
-                Create_time Update_time Check_time Create_options Comment /;
     my $db_command = "SHOW TABLE STATUS FROM loncapa LIKE '$tablename'";
     my $sth = $dbh->prepare($db_command);
     $sth->execute();
@@ -532,10 +581,16 @@ sub update_table_info {
         &disconnect_from_db();
         return undef;
     }
+    my @column_name = @{$sth->{NAME}};
     #
     my @info=$sth->fetchrow_array;
     for (my $i=0;$i<= $#info ; $i++) {
-        $Tables{$tablename}->{$tabledesc[$i]}= $info[$i];
+        if ($column_name[$i] =~ /^(Create_|Update_|Check_)time$/) {
+            $Tables{$tablename}->{$column_name[$i]}= 
+                &unsqltime($info[$i]);
+        } else {
+            $Tables{$tablename}->{$column_name[$i]}= $info[$i];
+        }
     }
     #
     # Determine the column order
@@ -559,6 +614,29 @@ sub update_table_info {
     $debugstring = "Retrieved table info for $tablename";
     return 1;
 }
+
+###############################
+
+=pod
+
+=item &table_information()
+
+Inputs: table id
+
+Returns: hash with the table status
+
+=cut
+
+###############################
+sub table_information {
+    my $table_id=shift;
+    if (&update_table_info($table_id)) {
+	return %{$Tables{$table_id}};
+    } else {
+	return ();
+    }
+}
+
 ###############################
 
 =pod
@@ -571,7 +649,7 @@ Returns: array with column order
 
 =cut
 
-
+###############################
 sub col_order {
     my $table_id=shift;
     if (&update_table_info($table_id)) {
@@ -580,6 +658,7 @@ sub col_order {
 	return ();
     }
 }
+
 ###############################
 
 =pod
@@ -587,9 +666,41 @@ sub col_order {
 =item &create_table()
 
 Inputs: 
-    table description
+    table description, see &build_table_creation_request
+Returns:
+    undef on error, table id on success.
 
-Input formats:
+=cut
+
+###############################
+sub create_table {
+    return undef if (!defined(&connect_to_db($dbh)));
+    my ($table_des)=@_;
+    my ($request,$table_id) = &build_table_creation_request($table_des);
+    #
+    # Execute the request to create the table
+    #############################################
+    my $count = $dbh->do($request);
+    if (! defined($count)) {
+        $errorstring = "$dbh ATTEMPTED:\n".$request."\nRESULTING ERROR:\n".
+            $dbh->errstr();
+        return undef;
+    }
+    my $tablename = &translate_id($table_id);
+    delete($Tables{$tablename}) if (exists($Tables{$tablename}));
+    return undef if (! defined(&update_table_info($table_id)));
+    $debugstring = "Created table $tablename at time ".time.
+        " with request\n$request";
+    return $table_id;
+}
+
+###############################
+
+=pod
+
+=item build_table_creation_request
+
+Input: table description
 
     table description = {
         permanent  => 'yes' or 'no',
@@ -621,14 +732,12 @@ Input formats:
 
     }
 
-Returns:
-    undef on error, table id on success.
+Returns: scalar string containing mysql commands to create the table
 
 =cut
 
 ###############################
-sub create_table {
-    return undef if (!defined(&connect_to_db($dbh)));
+sub build_table_creation_request {
     my ($table_des)=@_;
     #
     # Build request to create table
@@ -708,23 +817,27 @@ sub create_table {
     unless($table_des->{'permanent'} eq 'yes') {
         $request.="COMMENT = 'temporary' ";
     } 
-    $request .= "TYPE=MYISAM";
-    #
-    # Execute the request to create the table
-    #############################################
-    my $count = $dbh->do($request);
-    if (! defined($count)) {
-        $errorstring = "$dbh ATTEMPTED:\n".$request."\nRESULTING ERROR:\n";
-        return undef;
-    }
-    #
-    # Set up the internal bookkeeping
-    #############################################
-    delete($Tables{$tablename}) if (exists($Tables{$tablename}));
-    return undef if (! defined(&update_table_info($table_id)));
-    $debugstring = "Created table $tablename at time ".time.
-        " with request\n$request";
-    return $table_id;
+    $request .= "ENGINE=MYISAM";
+    return $request,$table_id;
+}
+
+###############################
+
+=pod
+
+=item &get_table_prefix()
+
+returns the cleaned version of user.name and user.domain for us in table names
+
+=cut
+
+###############################
+sub get_table_prefix {
+    my $clean_name   = $env{'user.name'};
+    my $clean_domain = $env{'user.domain'};
+    $clean_name =~ s/\W//g;
+    $clean_domain =~ s/\W//g;
+    return $clean_name.'_'.$clean_domain.'_';
 }
 
 ###############################
@@ -741,8 +854,9 @@ Used internally to prevent table name co
 sub get_new_table_id {
     my $newid = 0;
     my @tables = &tables_in_db();
+    my $prefix = &get_table_prefix();
     foreach (@tables) {
-        if (/^$ENV{'user.name'}_$ENV{'user.domain'}_(\d+)$/) {
+        if (/^\Q$prefix\E(\d+)$/) {
             $newid = $1 if ($1 > $newid);
         }
     }
@@ -855,6 +969,59 @@ sub store_row {
     return 1;
 }
 
+
+###############################
+
+=pod
+
+=item &bulk_store_rows()
+
+Inputs: table id, [columns],[[row data1].[row data2],...]
+
+returns undef on error, 1 on success.
+
+=cut
+
+###############################
+sub bulk_store_rows {
+    my ($table_id,$columns,$rows) = @_;
+    # 
+    return undef if (! defined(&connect_to_db()));
+    my $dbh = &get_dbh();
+    return undef if (! defined($dbh));
+    my $table_status = &check_table($table_id);
+    return undef if (! defined($table_status));
+    if (! $table_status) {
+        $errorstring = "table $table_id does not exist.";
+        return undef;
+    }
+    #
+    my $tablename = &translate_id($table_id);
+    #
+    my $request = 'INSERT IGNORE INTO '.$tablename.' ';
+    if (defined($columns) && ref($columns) eq 'ARRAY') {
+        $request .= join(',',@$columns).' ';
+    }
+    if (! defined($rows) || ref($rows) ne 'ARRAY') {
+        $errorstring = "no input rows given.";
+        return undef;
+    }
+    $request .= 'VALUES ';
+    foreach my $row (@$rows) {
+        # avoid doing row stuff here...
+        $request .= '('.join(',',@$row).'),';
+    }
+    $request =~ s/,$//;
+    # $debugstring = "Executed ".$/.$request; # commented out - this is big
+    $dbh->do($request);
+    if ($dbh->err) {
+        $errorstring = 'Attempted '.$/.$request.$/.'Got error '.$dbh->errstr();
+        return undef;
+    }
+    return 1;
+}
+
+
 ###############################
 
 =pod
@@ -928,16 +1095,29 @@ Returns undef on error.
 =cut
 
 ###########################################
+
+########## Show-Tables Cache
+my $have_read_tables = 0;
+my $dbh_sth;
+##########
+
 sub tables_in_db {
     return undef if (!defined(&connect_to_db()));
-    my $sth=$dbh->prepare('SHOW TABLES');
-    $sth->execute();
-    $sth->execute();
-    my $aref = $sth->fetchall_arrayref;
-    if ($sth->err()) {
+    
+    ########## Show-Tables Cache
+    if(!$have_read_tables) { 
+     $dbh_sth=$dbh->prepare('SHOW TABLES');
+     $have_read_tables = 1;
+    }   
+    $dbh_sth->execute();
+    #$dbh_sth->execute(); # Removed strange execute - from release 119
+    ##########    
+    
+    my $aref = $dbh_sth->fetchall_arrayref;
+    if ($dbh_sth->err()) {
         $errorstring = 
             "$dbh ATTEMPTED:\n".'fetchall_arrayref after SHOW TABLES'.
-            "\nRESULTING ERROR:\n".$sth->errstr;
+            "\nRESULTING ERROR:\n".$dbh_sth->errstr;
         return undef;
     }
     my @table_list;
@@ -968,7 +1148,7 @@ sub translate_id {
     # id should be a digit.  If it is not a digit we assume the given id
     # is complete and does not need to be translated.
     return $id if ($id =~ /\D/);  
-    return $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.$id;
+    return &get_table_prefix().$id;
 }
 
 ###########################################
@@ -1065,7 +1245,23 @@ sub drop_table {
     return 1; # if we got here there was no error, so return a 'true' value
 }
 
+##########################################
+
+=pod
+
+=item fix_table_name 
 
+Fixes a table name so that it will work with MySQL.
+
+=cut
+
+##########################################
+sub fix_table_name {
+    my ($name) = @_;
+    $name =~ s/^(\d+[eE]\d+)/_$1/;
+    $name =~ s/\W//g;
+    return $name;
+}
 
 
 # ---------------------------- convert 'time' format into a datetime sql format
@@ -1092,7 +1288,7 @@ sub unsqltime {
     my $timestamp=shift;
     if ($timestamp=~/^(\d+)\-(\d+)\-(\d+)\s+(\d+)\:(\d+)\:(\d+)$/) {
         $timestamp=&maketime('year'=>$1,'month'=>$2,'day'=>$3,
-                             'hours'=>$4,'minutes'=>$5,'seconds'=>$6);
+                             'hours'=>$4,'minutes'=>$5,'seconds'=>$6,'dlsav'=>-1);
     }
     return $timestamp;
 }