* * This file is part of Gombe-MI. * * Gombe-MI is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Gombe-MI. If not, see . * * Library of php functions supporting file upload for Gombe-MI. * * Karl O. Pinc */ // Includes require('constants.php.inc'); // Constants // Values for the 'action' form variable. define('ACTION_UPLOAD', 'u'); define('ACTION_NONE', ''); // Values for the login session keys. // Where the login timestamp is kept. define('LOGIN_TIMESTAMP', 'LOGIN_TIMESTAMP'); // How long, in seconds, is the user allowed to stay logged in // without refereshing the page? // (Note that this must be <= to the PHP session.cookie_lifetime.) define('LOGIN_INACTIVITY_TIMEOUT', 3600); // 1 hour // The (basename of the) last file loaded into the database. define('UPLOADED_FILE', 'UPLOADED_FILE'); // Values for the Upload_Connection class's savepoints. define('SP_NAME', 'SLOPPY_SAVEPOINT'); define('SP_COMMAND', 'SAVEPOINT ' . SP_NAME . ';'); ///////////////////////////////////////////////////////////////////// function maintain_login_session() { /* * Maintain the session variables throughout a login. * * Input: * * Returns: n/a * * Side Effects: * Messes with the PHP session. * * Remarks: * Use this instead of php's session_start() function. * * This uses a specific timestamp variable to determine * how long the login session lasts. This is safer than * relying on the browser to respect the session cookie lifetime. */ define('SESSION_NAME', 'GOMBEMI'); session_name(SESSION_NAME); session_start(); if (array_key_exists(LOGIN_TIMESTAMP, $_SESSION) && time() > ($_SESSION[LOGIN_TIMESTAMP] + LOGIN_INACTIVITY_TIMEOUT)) { // The user has been inactive too long. // Erase their session. // Unset all of the session variables. $_SESSION = array(); } // Update the login timestamp so we know when we were last seen. $_SESSION[LOGIN_TIMESTAMP] = time(); // Reset the cookie lifetime so it stays around. $cookie_lifetime = ini_get('session.cookie_lifetime'); $cookie_path = ini_get('session.cookie_path'); if ($cookie_lifetime > 0) { // Extend cookie lifetime. // For unknown reasons this breaks without the right session id. // (The cookie path and domain must match or the cookie won't be updated.) setcookie(session_name(), session_id(), time() + $cookie_lifetime, $cookie_path); } return; } ///////////////////////////////////////////////////////////////////// function show_html_start($title) { /* * Start the html on a page. * * Input: * $title The window title to use. * * Returns: n/a * * Side Effects: * Outputs html. */ ?> <?php print($title); ?> db = $db; $this->user = $user; $this->password = $password; $this->desc = $desc; $this->extra_input = array(); $this->errors = ''; return; } function error($l, $e) { /* * Syntax: error($l, $e, $faulting, $desc) * * Save an error to display on the form. * * Input: * $l Line number in the input file, or ''. * $e HTML string containing an error message. * Must be able to be put in a HTML paragraph block. * $faulting Boolean. Optional. Defaults to FALSE. * TRUE when this error may be a false positive. * $desc Optional. Defaults to ''. * Extra description of the error. * * Class variables: * $errors * * Side Effects: * Updates $this->errors. */ // // Parse args. // if (func_num_args() > 2) { $faulting = func_get_arg(2); } else { $faulting = FALSE; } if (func_num_args() > 3) { $desc = func_get_arg(3); } else { $desc = ''; } // Initialize if ($desc != '') { $desc = ': ' . $desc; } if ($l == '') { $line_desc = ''; } else { $line_desc = "Line $l: "; } // Save the error. $this->errors .= '

' . $this->make_caution($faulting) . $line_desc . '' . $e . "$desc

\n"; return; } function make_caution($faulting) { /* * Syntax: make_caution($faulting) * * Return a blurb when an error message may be an artifact. * * Input: * $faulting Boolean. When TRUE input data was ignored. * * Returns: * A htmlized string describing the problem. * * Side Effects: */ if ($faulting) { // We already had one error, so subsequent problems may or // may not be real. $prefix = 'CAUTION -- This error may not be real; prior row(s) of the uploaded file were rejected:
'; } else { $prefix = ''; } return $prefix; } function show() { /* * Syntax: show([$error [, $password]]) * * Show the upload form. * * Input: * $error (optional) A HTML string with an error message. * $password (optional) A database password to use in place of * the class variable. * * Class variables: * $db * $user * $password * $desc * $extra_input * $errors (cleared) * * * Returns: n/a * * Side Effects: * Outputs html. * Form variables: * action ACTION_UPLOAD on submission * db Database name * user Database user * password Database password * datafile Uploaded file * Current page re-invoked up submission. * * * Remarks: * Shows the error, or $this->errors, if any, at the top of the page. * * If any of $this->db, $this->user, or the password used * are the empty string then the user * is allowed to login again. Otherwise, only the database * can be changed. * * One of the main pages of the program. */ // // Get arguments, if any. // // The error message if (func_num_args() > 0) { $error = func_get_arg(0); } else { $error = ''; } $error = $this->errors . $error; $this->errors = ''; // reset for next form display // The password if (func_num_args() > 1) { $password = func_get_arg(1); } else { $password = $this->password; } // // Display the error message, if any. // if ($error !== '') { if (isset($_FILES['datafile']) && isset($_FILES['datafile']['name'])) { print '

File supplied: ' . htmlentities($_FILES['datafile']['name'], ENT_QUOTES) . '

'; } print $error; print "
\n"; } // // Ouput a form where the user can upload files to compare. // ?>
db == '' || $this->user == '' || $password == '') { ?> db == '' || $this->user == '' || $password == '') { ?> extra_input as $more_html) { $tabindex++; print ''; if (array_key_exists('label', $more_html)) print ''; if (array_key_exists('data', $more_html)) print ''; print ''; } ?>
User: user; ?>
' . $more_html['label'] . '' . $more_html['data'] . '

upload_form =& $upload_form; if (array_key_exists('datafile', $_FILES)) { $this->filename = htmlentities($_FILES['datafile']['name'], ENT_QUOTES); } else { $this->filename = ''; } } function success() { /* * Note a successful upload into the database. * * Input: * * Returns: n/a * * Side Effects: * Remembers the file successfully uploaded * * Remarks: * Must be called so we know not to allow the same file * to be loaded into the same db twice (in a row.) */ $_SESSION[UPLOADED_FILE][$this->upload_form->db] = $this->filename; return; } function upload() { /* * Get the uploaded data and clean it up. * * Input: * * Returns: * TRUE when file successfully uploaded, FALSE otherwise. * * Sets: * $data Array containing the uploaded data file. * * Side Effects: * Calls $upload_form->show() to output html when there's an error. * * HTML form variables: * datafile Uploaded file */ // Do a little de-referencing up-front. $upload_form = $this->upload_form; // Get uploaded data // $datafile $datafile = $this->filename; if ($datafile !== '') { $datafile_tmp = $_FILES['datafile']['tmp_name']; $datafile_err = $_FILES['datafile']['error']; } else { $datafile_tmp = ''; $datafile_err = UPLOAD_ERR_NO_FILE; } // Get uploaded file. switch ($datafile_err) { case UPLOAD_ERR_OK: break; case UPLOAD_ERR_NO_FILE: // No file was uploaded. $upload_form->show('

Error uploading file: No file uploaded

'); return FALSE; default: // Got an error on the file upload. // Let the user try again. $upload_form->show('

' . explain_error('Error uploading file', $datafile_err) . '

'); return FALSE; } if (array_key_exists(UPLOADED_FILE, $_SESSION) && array_key_exists($upload_form->db , $_SESSION[UPLOADED_FILE]) && $this->filename == $_SESSION[UPLOADED_FILE][$upload_form->db]) { // The user is trying to upload a file (with the same name as) // the file which was last successfully put into the database. $upload_form->show('

Upload aborted. A file with this name was the last file successfully loaded into the ' . $upload_form->db . ' database.

'); return FALSE; } if ($_FILES['datafile']['size'] == 0) { // Let the user try again. $upload_form->show('

The file uploaded' . " ($datafile) is empty.

"); return FALSE; } // Get rid of OS specific cruft. $tmpf = '/tmp/upcen.' . getmypid(); if (move_and_fix_eol($datafile_tmp, $tmpf) != 0) { // Let the user try again. $upload_form->show('

Unexpected failure trying' . 'to move uploaded file.

'); return FALSE; } // Read the file $content = file($tmpf); unlink($tmpf); // cleanup if ($content === FALSE) { // Let the user try again. $upload_form->show('

Unexpected failure trying' . ' to read uploaded file.

'); return FALSE; } // Get rid of any stupid MS Windows eof character $last = count($content) - 1; if (substr($content[$last], strlen($content[$last]) - 1, 1) == chr(26)) { // File ends in a Ctrl-Z, the DOS EOF character. // Remove it. $content[$last] = substr($content[$last] , 0 , strlen($content[$last]) - 1); } $this->data = $content; return TRUE; } } ///////////////////////////////////////////////////////////////////// class Upload_connection { /* * Abstracts database operations over a postgresql connection. */ /* * Bugs: * It does not seem (php-4.3.9-3.22) that you can cache * a query result resource in an object. So we've two * different functions sql() and sql_result(). * This could just be the result of php4 stupidness with * objects and references. I've not thought it through. */ // Private variables. var $upload_form; // An Upload_form instance. var $pg; // The postgresql connection handle. var $db; // The database connected to. var $user; // The user used to authenticate. var $password; // The password used to authenticate. var $last_pg_error; // The last error from postgres // Set to '' by any function that might cause a pg // error, except when we know that we're going to // do operations that might trash pg_last_error(). function Upload_connection($db, $user, $password, &$upload_form) { /* * Class constructor. * * Input: * $db The database to connect to. * $user The username to login with. * $password The password to login with. * $upload_form The HTML form used to get info from the user. * * Returns: A class instance. * * Side Effects: none * * Remarks: */ // Save the instance varaibles.. $this->upload_form =& $upload_form; $this->db = $db; $this->user = $user; $this->password = $password; unset($this->last_pg_error); return; } function connect() { /* * Connect to the database * * Input: * * Returns: * TRUE on success, FALSE otherwise. * * Globals: * $last_pg_error Cleared. * * Side Effects: * Connects to the db with the username and password. * On error re-displays html form with error. */ // // Login to postgres. // $this->pg = pg_connect('host=' . PG_HOST . ' port=' . PG_PORT . " dbname=$this->db user=$this->user password='" . str_replace("'" , "\\'" , str_replace('\\', '\\' . '\\', $this->password)) . "'"); if ($this->pg === FALSE) { // Let the user try again. // Allow user to login again by calling with blank password. $this->upload_form->show('

Login failed.' . ' Is the database, user,' . ' and password correct?

' , ''); return FALSE; } $this->sloppy = FALSE; unset($this->last_pg_error); return TRUE; } function begin() { /* * Begin a transation. * * Syntax: begin() * begin(sloppy_errors) * * Input: * sloppy_errors (boolean, optional, defaults to FALSE) * When TRUE simulate the ability to continue executing statements * within a transaction after an error, reporting a new error * for each error that occurs. Queries can still fail but * subsequent queries within the simulated transaction can * appear to succeed, although they will never be committed * to the database. * * Note that this works by rolling back on error and starting a * new transaction that will never be committed. So all statements * executed after a failure will not see the effects of statements * executed before the failure. This can produce errors that would * not otherwise exist. * * Returns: * TRUE on success, FALSE otherwise. * * Globals: * $last_pg_error Cleared. * * Side Effects: * Begins a db transaction. * Can establish a savepoint. * On error re-displays html form with error. */ if (func_num_args() > 0) { $sloppy_errors = func_get_arg(0); } else { $sloppy_errors = FALSE; } $this->sloppy = $sloppy_errors; $this->faulting = FALSE; unset($this->last_pg_error); // // Start the transaction. // if (pg_query($this->pg, 'BEGIN;') === FALSE) { // Let the user try again. $this->upload_form->show('

Failed to begin transaction:' . '
' . htmlentities(pg_last_error($this->pg)) . '

'); return FALSE; } if ($this->sloppy) { // We need a savepoint so we've one to release. if (pg_query($this->pg, SP_COMMAND) === FALSE) { $this->upload_form->show('

Failed to establish savepoint at' . ' transaction start:
' . htmlentities(pg_last_error($this->pg)) . '

'); return FALSE; } } return TRUE; } function rollback($error) { /* * Rollback the ongoing transation. * * Input: * $error A HTML string with an error message. * * Returns: * TRUE on success, FALSE otherwise. * * Globals: * $last_pg_error Cleared. * * Side Effects: * Rolls back the db transaction. * Re-displays html form with error allowing user another chance. */ // DEBUG //error_log($error); // Give up. if (pg_query($this->pg, 'ROLLBACK;') === FALSE) { // Tell user original error and that the rollback failed. $error .= '

Failed to rollback transaction:' . '
' . htmlentities(pg_last_error($this->pg)) . '

'; } $this->upload_form->show($error); $this->sloppy = FALSE; unset($this->last_pg_error); return; } function sloppy_restart() { /* * Private function, internal use only. * * Restart a sloppy transaction after an error * * Input: * * Returns: n/a * * Globals: * $last_pg_error Set. * * Side Effects: * Rolls back transaction that caused an error to the savepoint. * Begins a new transaction. * Remembers that the sloppy transaction should fail. * Can abort the entire script. */ // In a sloppy transaction. $this->faulting = TRUE; $this->last_pg_error = pg_last_error($this->pg); // Rollback the existing transaction. if (pg_query($this->pg, 'ROLLBACK TO SAVEPOINT ' . SP_NAME . ';') === FALSE) { error_log('Warning: Failed to roll sloppy transaction back to savepoint' + ' after error'); $this->upload_form->show('

Program aborted: Internal error:' . ' Failed to rollback to savepoint

'); exit(); } return; } function redo_savepoint() { /* * Redo the savepoint we use for sloppy transactions. * * Input: * * Returns: n/a/ * * Side Effects: * May send html with an error to the user and abort. * Re-establishes the savepoint. * * Remarks: * $last_pg_error not altered because we only call this after * we know that the last query succeeded. */ if (pg_query($this->pg, 'RELEASE SAVEPOINT ' . SP_NAME . ';') === FALSE) { error_log('Program aborted: Internal error: Failed to release savepoint'); $this->upload_form->show('

Internal error:' . ' Failed to release savepoint'); exit(); } if (pg_query($this->pg, SP_COMMAND) === FALSE) { error_log('Program aborted: Internal error: Failed to establish savepoint'); $this->upload_form->show('

Internal error:' . ' Failed to establish savepoint'); exit(); } return; } function sql($sql) { /* * Submit some SQL to the db. * * Syntax: sql($sql) * sql($sql, $side_effects) * * Input: * $sql The SQL to send to the db. * $side_effects Boolean. Optional. Defaults to TRUE. * When true the sql may alter the db. * * Returns: * TRUE if the SQL executed successfully, FALSE otherwise. * * Globals: * $last_pg_error Cleared. * * Side Effects: * SQL may alter db. */ if (func_num_args() > 1) { $side_effects = func_get_arg(1); } else { $side_effects = TRUE; } unset($this->last_pg_error); if (pg_query($this->pg, $sql) === FALSE) { error_log("Failed query: $sql"); if ($this->sloppy === TRUE) { // In a sloppy transaction. $this->sloppy_restart(); } return FAlSE; } if ($this->sloppy === TRUE && $side_effects) { // Get rid of the old savepoint and make a new one. $this->redo_savepoint(); } return TRUE; } function sql_result($sql) { /* * Submit some SQL to the db and get a result. * * Syntax: sql_result($sql) * sql_result($sql, $side_effects) * * Input: * $sql The SQL to send to the db. * $side_effects Boolean. Optional. Defaults to FALSE. * When true the sql may alter the db. * * Returns: * The first column of the first row of the most recent * query result set or FALSE on error or the integer 0 * if there is no result. * * Globals: * $last_pg_error Cleared. * * Side Effects: * SQL may alter db. * * Remarks: * Values are returned as strings. Distingush no rows from the string * "0" using ===. * * Bugs: * Booleans are returned as the strings "t" and "f" and NULL as * the string "NULL". */ if (func_num_args() > 1) { $side_effects = func_get_arg(1); } else { $side_effects = FALSE; } unset($this->last_pg_error); $qr = pg_query($this->pg, $sql); if ($qr === FALSE) { error_log("Failed query: $sql"); if ($this->sloppy === TRUE) { // In a sloppy transaction. $this->sloppy_restart(); } return FALSE; } if (pg_num_rows($qr) == 0) { return 0; } // Stash the result. $r = pg_fetch_result($qr, 0, 0); if ($this->sloppy === TRUE && $side_effects) { // Get rid of the old savepoint and make a new one. $this->redo_savepoint(); } return $r; } function last_error() { /* * Return string describing the last error on the connection. * * Input: * * Returns: * String describing last error on the connection. * * Globals: * $last_pg_error Referenced. * * Side Effects: */ if (isset($this->last_pg_error)) { return $this->last_pg_error; } return pg_last_error($this->pg); } function sloppy_rollback() { /* * Private function, for internal use only. * * Rollback the current (sloppy) transaction. * * Input: * * Returns: n/a * * Globals: * $last_pg_error Cleared. * * Side Effects: * Rolls back the current transaction. * * Remarks: * Called only when in a sloppy transaction that has been continued * and has 'completed' without further error. * */ // The sloppy transaction had a failure. // Rollback the 'continued' transaction. if (pg_query($this->pg, 'ROLLBACK;') === FALSE) { error_log('The sloppy transaction could not be rolled back'); // This is kinda bad, we don't want to commit things that we shouldn't. exit('Internal error: Failed to rollback a sloppy transaction'); } unset($this->last_pg_error); return; } function commit() { /* * Commit the current transaction. * * Input: * * Returns: * TRUE on success or FALSE on failure. * * Globals: * $last_pg_error Cleared. * * Side Effects: * Completes the current transaction. * */ if ($this->sloppy && $this->faulting) { // The sloppy transaction had a failure. $this->sloppy_rollback(); return FALSE; // The transaction did not commit. } unset($this->last_pg_error); if (pg_query($this->pg, 'COMMIT;') === FALSE) { return FALSE; } return TRUE; } function complete() { /* * Commit the current transaction, if possible. * * Input: * * Returns: * TRUE if the transaction completed or FALSE if it did not. * * Globals: * $last_pg_error Cleared. * * Side Effects: * Completes the current transaction. * * Remarks: * If the transaction won't commit, roll it back instead. */ if ($this->sloppy && $this->faulting) { // The sloppy transaction had a failure. $this->sloppy_rollback(); return FALSE; // The sloppy transaction did not commit. } if (!$this->commit()) { $this->rollback('

Failed to commit transaction:' . '
' . htmlentities(pg_last_error($this->pg)) . '

'); return FALSE; } unset($this->last_pg_error); return TRUE; } }