2 /* Copyright (C) 2012, 2013 The Meme Factory, Inc. http://www.meme.com/
3 * Copyright (C) 2007-2009, 2011, Karl O. Pinc <kop@meme.com>
5 * This file is part of Gombe-MI.
7 * Gombe-MI is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Gombe-MI. If not, see <http://www.gnu.org/licenses/>.
20 * Library of php functions supporting file upload for Gombe-MI.
22 * Karl O. Pinc <kop@meme.com>
26 require('constants.php.inc');
30 // Values for the 'action' form variable.
31 define('ACTION_UPLOAD', 'u');
32 define('ACTION_NONE', '');
34 // Values for the login session keys.
36 // Where the login timestamp is kept.
37 define('LOGIN_TIMESTAMP', 'LOGIN_TIMESTAMP');
38 // How long, in seconds, is the user allowed to stay logged in
39 // without refereshing the page?
40 // (Note that this must be <= to the PHP session.cookie_lifetime.)
41 define('LOGIN_INACTIVITY_TIMEOUT', 3600); // 1 hour
42 // The (basename of the) last file loaded into the database.
43 define('UPLOADED_FILE', 'UPLOADED_FILE');
45 // Values for the Upload_Connection class's savepoints.
46 define('SP_NAME', 'SLOPPY_SAVEPOINT');
47 define('SP_COMMAND', 'SAVEPOINT ' . SP_NAME . ';');
49 /////////////////////////////////////////////////////////////////////
50 function maintain_login_session() {
52 * Maintain the session variables throughout a login.
59 * Messes with the PHP session.
62 * Use this instead of php's session_start() function.
64 * This uses a specific timestamp variable to determine
65 * how long the login session lasts. This is safer than
66 * relying on the browser to respect the session cookie lifetime.
69 define('SESSION_NAME', 'GOMBEMI');
71 session_name(SESSION_NAME);
74 if (array_key_exists(LOGIN_TIMESTAMP, $_SESSION)
75 && time() > ($_SESSION[LOGIN_TIMESTAMP] + LOGIN_INACTIVITY_TIMEOUT)) {
76 // The user has been inactive too long.
77 // Erase their session.
79 // Unset all of the session variables.
83 // Update the login timestamp so we know when we were last seen.
84 $_SESSION[LOGIN_TIMESTAMP] = time();
86 // Reset the cookie lifetime so it stays around.
87 $cookie_lifetime = ini_get('session.cookie_lifetime');
88 $cookie_path = ini_get('session.cookie_path');
90 if ($cookie_lifetime > 0) {
91 // Extend cookie lifetime.
92 // For unknown reasons this breaks without the right session id.
93 // (The cookie path and domain must match or the cookie won't be updated.)
94 setcookie(session_name(), session_id(), time() + $cookie_lifetime, $cookie_path);
99 /////////////////////////////////////////////////////////////////////
100 function show_html_start($title) {
102 * Start the html on a page.
105 * $title The window title to use.
113 ?><?xml version="1.0" encoding="UTF-8" standalone="no"?>
114 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
115 <html xmlns="http://www.w3.org/1999/xhtml">
117 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
118 <title><?php print($title); ?></title>
119 <link rel="stylesheet" href="/gombemi.css" type="text/css" />
128 /////////////////////////////////////////////////////////////////////
129 function show_html_finish() {
131 * Finish the html on a page.
151 /////////////////////////////////////////////////////////////////////
152 function show_navbar() {
154 * Show the stock navbar for page top and bottom.
166 <a href="<?php print HOME_TARGET; ?>">HOME</a> | <a href="<?php print PROGRAM_BASE; ?>logout">Logout</a>
173 /////////////////////////////////////////////////////////////////////
174 function get_session_var($varname, $escape_html) {
176 * Get a HTML form varaiable that's kept in in the session.
179 * $varname Name of the form/session variable.
180 * $escape_html When true, escape all html special chars in the
181 * HTML form variable before saving it in the session
182 * or returning it. Also remove leading and trailing
186 * The value of the form variable if present, otherwise the value
187 * of the session varaible, if present, otherwise "".
189 * Uses (reads and sets):
190 * $SESSION[$varname] Set to the HTML form varaible value (if any).
194 if (array_key_exists($varname, $_REQUEST)) {
195 $v = $_REQUEST[$varname];
197 $v = htmlentities(trim($v), ENT_QUOTES);
199 $_SESSION[$varname] = $v;
201 if (array_key_exists($varname, $_SESSION)) {
202 $v = $_SESSION[$varname];
211 /////////////////////////////////////////////////////////////////////
217 /////////////////////////////////////////////////////////////////////
220 * Abstraction of the html form that asks what data to upload where by who.
223 // Private variables. (Set 'em and forget 'em.)
224 var $db; // The database to connect to.
225 var $user; // The userid of the user (htmlescaped).
226 var $password; // The user's password.
227 var $desc; // Description (name) of data to upload.
229 var $errors; // String with htmlized error text.
232 var $extra_input; // Extra html, one element for each data item,
233 // to add at the bottom of the form.
234 // Each array item is an array, which may have
235 // either of the following two keys:
236 // 'label', containing a textual label
237 // 'data', containing the form to input data
239 function Upload_form($db, $user, $password, $desc) {
242 $this->password = $password;
244 $this->extra_input = array();
251 function error($l, $e) {
253 * Syntax: error($l, $e, $faulting, $desc)
255 * Save an error to display on the form.
258 * $l Line number in the input file, or ''.
259 * $e HTML string containing an error message.
260 * Must be able to be put in a HTML paragraph block.
261 * $faulting Boolean. Optional. Defaults to FALSE.
262 * TRUE when this error may be a false positive.
263 * $desc Optional. Defaults to ''.
264 * Extra description of the error.
270 * Updates $this->errors.
276 if (func_num_args() > 2) {
277 $faulting = func_get_arg(2);
282 if (func_num_args() > 3) {
283 $desc = func_get_arg(3);
290 $desc = ': ' . $desc;
296 $line_desc = "Line $l: ";
300 $this->errors .= '<p>'
301 . $this->make_caution($faulting)
303 . '<em class="error">'
305 . "</em>$desc</p>\n";
309 function make_caution($faulting) {
311 * Syntax: make_caution($faulting)
313 * Return a blurb when an error message may be an artifact.
316 * $faulting Boolean. When TRUE input data was ignored.
319 * A htmlized string describing the problem.
325 // We already had one error, so subsequent problems may or
327 $prefix = 'CAUTION -- This error may not be real; prior row(s) of the uploaded file were rejected:<br />';
337 * Syntax: show([$error [, $password]])
339 * Show the upload form.
342 * $error (optional) A HTML string with an error message.
343 * $password (optional) A database password to use in place of
344 * the class variable.
360 * action ACTION_UPLOAD on submission
363 * password Database password
364 * datafile Uploaded file
365 * Current page re-invoked up submission.
369 * Shows the error, or $this->errors, if any, at the top of the page.
371 * If any of $this->db, $this->user, or the password used
372 * are the empty string then the user
373 * is allowed to login again. Otherwise, only the database
376 * One of the main pages of the program.
380 // Get arguments, if any.
384 if (func_num_args() > 0) {
385 $error = func_get_arg(0);
390 $error = $this->errors . $error;
391 $this->errors = ''; // reset for next form display
394 if (func_num_args() > 1) {
395 $password = func_get_arg(1);
397 $password = $this->password;
401 // Display the error message, if any.
404 if (isset($_FILES['datafile']) && isset($_FILES['datafile']['name'])) {
405 print '<p>File supplied: <em class="filename">'
406 . htmlentities($_FILES['datafile']['name'], ENT_QUOTES)
414 // Ouput a form where the user can upload files to compare.
418 <form action="" enctype="multipart/form-data" method="post">
422 value="<?php print ACTION_UPLOAD; ?>"
429 <label for="db_id">Database:</label>
437 value="<?php print $this->db; ?>"
443 if ($this->db == '' || $this->user == '' || $password == '') {
446 <label for="user_id">User:</label>
454 value="<?php print $this->user; ?>"
474 if ($this->db == '' || $this->user == '' || $password == '') {
478 <label for="password_id">Password:</label>
481 <input name="password"
492 // Display the extra input fields
493 foreach ($this->extra_input as $more_html) {
496 if (array_key_exists('label', $more_html))
497 print '<td class="label">' . $more_html['label'] . '</td>';
498 if (array_key_exists('data', $more_html))
499 print '<td>' . $more_html['data'] . '</td>';
505 <label for="datafile_id">
506 File with <?php print($this->desc); ?>:
512 <input name="datafile"
513 tabindex="<?php print $tabindex; ?>"
523 <input value="Upload" tabindex="20" type="submit" />
532 /////////////////////////////////////////////////////////////////////
535 * Abstraction of the data that was uploaded.
538 * Uses the HTML form variables of the Upload_form class.
545 // Private variables.
548 function Uploaded_data(&$upload_form) {
553 * $upload_form An instance of the Upload_form class.
561 $this->upload_form =& $upload_form;
563 if (array_key_exists('datafile', $_FILES)) {
564 $this->filename = htmlentities($_FILES['datafile']['name'], ENT_QUOTES);
566 $this->filename = '';
572 * Note a successful upload into the database.
579 * Remembers the file successfully uploaded
582 * Must be called so we know not to allow the same file
583 * to be loaded into the same db twice (in a row.)
586 $_SESSION[UPLOADED_FILE][$this->upload_form->db] = $this->filename;
593 * Get the uploaded data and clean it up.
598 * TRUE when file successfully uploaded, FALSE otherwise.
601 * $data Array containing the uploaded data file.
604 * Calls $upload_form->show() to output html when there's an error.
606 * HTML form variables:
607 * datafile Uploaded file
610 // Do a little de-referencing up-front.
611 $upload_form = $this->upload_form;
616 $datafile = $this->filename;
617 if ($datafile !== '') {
618 $datafile_tmp = $_FILES['datafile']['tmp_name'];
619 $datafile_err = $_FILES['datafile']['error'];
622 $datafile_err = UPLOAD_ERR_NO_FILE;
625 // Get uploaded file.
627 switch ($datafile_err) {
631 case UPLOAD_ERR_NO_FILE:
632 // No file was uploaded.
633 $upload_form->show('<p class="error">Error uploading file: No file uploaded</p>');
637 // Got an error on the file upload.
638 // Let the user try again.
639 $upload_form->show('<p class="error">'
640 . explain_error('Error uploading file', $datafile_err)
645 if (array_key_exists(UPLOADED_FILE, $_SESSION)
646 && array_key_exists($upload_form->db
647 , $_SESSION[UPLOADED_FILE])
649 == $_SESSION[UPLOADED_FILE][$upload_form->db]) {
650 // The user is trying to upload a file (with the same name as)
651 // the file which was last successfully put into the database.
652 $upload_form->show('<p class="error">Upload aborted. A file with this name was the last file successfully loaded into the ' . $upload_form->db . ' database.</p>');
656 if ($_FILES['datafile']['size'] == 0) {
657 // Let the user try again.
658 $upload_form->show('<p class="error">The file uploaded'
659 . " ($datafile) is empty.</p>");
663 // Get rid of OS specific cruft.
664 $tmpf = '/tmp/upcen.' . getmypid();
665 if (move_and_fix_eol($datafile_tmp, $tmpf) != 0) {
666 // Let the user try again.
667 $upload_form->show('<p class="error">Unexpected failure trying'
668 . 'to move uploaded file.</p>');
673 $content = file($tmpf);
674 unlink($tmpf); // cleanup
675 if ($content === FALSE) {
676 // Let the user try again.
677 $upload_form->show('<p class="error">Unexpected failure trying'
678 . ' to read uploaded file.</p>');
682 // Get rid of any stupid MS Windows eof character
683 $last = count($content) - 1;
684 if (substr($content[$last], strlen($content[$last]) - 1, 1) == chr(26)) {
685 // File ends in a Ctrl-Z, the DOS EOF character.
687 $content[$last] = substr($content[$last]
689 , strlen($content[$last]) - 1);
692 $this->data = $content;
698 /////////////////////////////////////////////////////////////////////
699 class Upload_connection {
702 * Abstracts database operations over a postgresql connection.
707 * It does not seem (php-4.3.9-3.22) that you can cache
708 * a query result resource in an object. So we've two
709 * different functions sql() and sql_result().
710 * This could just be the result of php4 stupidness with
711 * objects and references. I've not thought it through.
714 // Private variables.
715 var $upload_form; // An Upload_form instance.
716 var $pg; // The postgresql connection handle.
717 var $db; // The database connected to.
718 var $user; // The user used to authenticate.
719 var $password; // The password used to authenticate.
720 var $last_pg_error; // The last error from postgres
721 // Set to '' by any function that might cause a pg
722 // error, except when we know that we're going to
723 // do operations that might trash pg_last_error().
725 function Upload_connection($db, $user, $password, &$upload_form) {
730 * $db The database to connect to.
731 * $user The username to login with.
732 * $password The password to login with.
733 * $upload_form The HTML form used to get info from the user.
735 * Returns: A class instance.
742 // Save the instance varaibles..
743 $this->upload_form =& $upload_form;
746 $this->password = $password;
747 unset($this->last_pg_error);
754 * Connect to the database
759 * TRUE on success, FALSE otherwise.
762 * $last_pg_error Cleared.
765 * Connects to the db with the username and password.
766 * On error re-displays html form with error.
770 // Login to postgres.
772 $this->pg = pg_connect('host=' . PG_HOST
774 . " dbname=$this->db user=$this->user password='"
777 , str_replace('\\', '\\' . '\\', $this->password))
779 if ($this->pg === FALSE) {
780 // Let the user try again.
781 // Allow user to login again by calling with blank password.
782 $this->upload_form->show('<p> <em class="error">Login failed.</em>'
783 . ' Is the database, user,'
784 . ' and password correct?</p>'
789 $this->sloppy = FALSE;
790 unset($this->last_pg_error);
797 * Begin a transation.
800 * begin(sloppy_errors)
803 * sloppy_errors (boolean, optional, defaults to FALSE)
804 * When TRUE simulate the ability to continue executing statements
805 * within a transaction after an error, reporting a new error
806 * for each error that occurs. Queries can still fail but
807 * subsequent queries within the simulated transaction can
808 * appear to succeed, although they will never be committed
811 * Note that this works by rolling back on error and starting a
812 * new transaction that will never be committed. So all statements
813 * executed after a failure will not see the effects of statements
814 * executed before the failure. This can produce errors that would
815 * not otherwise exist.
818 * TRUE on success, FALSE otherwise.
821 * $last_pg_error Cleared.
824 * Begins a db transaction.
825 * Can establish a savepoint.
826 * On error re-displays html form with error.
829 if (func_num_args() > 0) {
830 $sloppy_errors = func_get_arg(0);
832 $sloppy_errors = FALSE;
835 $this->sloppy = $sloppy_errors;
836 $this->faulting = FALSE;
837 unset($this->last_pg_error);
840 // Start the transaction.
842 if (pg_query($this->pg, 'BEGIN;') === FALSE) {
843 // Let the user try again.
844 $this->upload_form->show('<p>Failed to begin transaction:'
845 . '<br /><em class="error">'
846 . htmlentities(pg_last_error($this->pg))
852 // We need a savepoint so we've one to release.
853 if (pg_query($this->pg, SP_COMMAND) === FALSE) {
854 $this->upload_form->show('<p>Failed to establish savepoint at'
855 . ' transaction start:<br /><em class="error">'
856 . htmlentities(pg_last_error($this->pg))
866 function rollback($error) {
868 * Rollback the ongoing transation.
871 * $error A HTML string with an error message.
874 * TRUE on success, FALSE otherwise.
877 * $last_pg_error Cleared.
880 * Rolls back the db transaction.
881 * Re-displays html form with error allowing user another chance.
888 if (pg_query($this->pg, 'ROLLBACK;') === FALSE) {
889 // Tell user original error and that the rollback failed.
890 $error .= '<p>Failed to rollback transaction:'
891 . '<br /><em class="error">'
892 . htmlentities(pg_last_error($this->pg))
896 $this->upload_form->show($error);
897 $this->sloppy = FALSE;
898 unset($this->last_pg_error);
903 function sloppy_restart() {
905 * Private function, internal use only.
907 * Restart a sloppy transaction after an error
914 * $last_pg_error Set.
917 * Rolls back transaction that caused an error to the savepoint.
918 * Begins a new transaction.
919 * Remembers that the sloppy transaction should fail.
920 * Can abort the entire script.
923 // In a sloppy transaction.
924 $this->faulting = TRUE;
925 $this->last_pg_error = pg_last_error($this->pg);
927 // Rollback the existing transaction.
928 if (pg_query($this->pg, 'ROLLBACK TO SAVEPOINT ' . SP_NAME . ';') === FALSE) {
929 error_log('Warning: Failed to roll sloppy transaction back to savepoint'
931 $this->upload_form->show('<p class="error">Program aborted: Internal error:'
932 . ' Failed to rollback to savepoint</p>');
939 function redo_savepoint() {
941 * Redo the savepoint we use for sloppy transactions.
948 * May send html with an error to the user and abort.
949 * Re-establishes the savepoint.
952 * $last_pg_error not altered because we only call this after
953 * we know that the last query succeeded.
956 if (pg_query($this->pg, 'RELEASE SAVEPOINT ' . SP_NAME . ';') === FALSE) {
957 error_log('Program aborted: Internal error: Failed to release savepoint');
958 $this->upload_form->show('<p class="error">Internal error:'
959 . ' Failed to release savepoint');
962 if (pg_query($this->pg, SP_COMMAND) === FALSE) {
963 error_log('Program aborted: Internal error: Failed to establish savepoint');
964 $this->upload_form->show('<p class="error">Internal error:'
965 . ' Failed to establish savepoint');
974 * Submit some SQL to the db.
977 * sql($sql, $side_effects)
980 * $sql The SQL to send to the db.
981 * $side_effects Boolean. Optional. Defaults to TRUE.
982 * When true the sql may alter the db.
985 * TRUE if the SQL executed successfully, FALSE otherwise.
988 * $last_pg_error Cleared.
994 if (func_num_args() > 1) {
995 $side_effects = func_get_arg(1);
997 $side_effects = TRUE;
1000 unset($this->last_pg_error);
1002 if (pg_query($this->pg, $sql) === FALSE) {
1003 error_log("Failed query: $sql");
1005 if ($this->sloppy === TRUE) {
1006 // In a sloppy transaction.
1007 $this->sloppy_restart();
1013 if ($this->sloppy === TRUE && $side_effects) {
1014 // Get rid of the old savepoint and make a new one.
1015 $this->redo_savepoint();
1021 function sql_result($sql) {
1023 * Submit some SQL to the db and get a result.
1025 * Syntax: sql_result($sql)
1026 * sql_result($sql, $side_effects)
1029 * $sql The SQL to send to the db.
1030 * $side_effects Boolean. Optional. Defaults to FALSE.
1031 * When true the sql may alter the db.
1034 * The first column of the first row of the most recent
1035 * query result set or FALSE on error or the integer 0
1036 * if there is no result.
1039 * $last_pg_error Cleared.
1045 * Values are returned as strings. Distingush no rows from the string
1049 * Booleans are returned as the strings "t" and "f" and NULL as
1050 * the string "NULL".
1053 if (func_num_args() > 1) {
1054 $side_effects = func_get_arg(1);
1056 $side_effects = FALSE;
1059 unset($this->last_pg_error);
1060 $qr = pg_query($this->pg, $sql);
1062 if ($qr === FALSE) {
1063 error_log("Failed query: $sql");
1065 if ($this->sloppy === TRUE) {
1066 // In a sloppy transaction.
1067 $this->sloppy_restart();
1073 if (pg_num_rows($qr) == 0) {
1077 // Stash the result.
1078 $r = pg_fetch_result($qr, 0, 0);
1080 if ($this->sloppy === TRUE && $side_effects) {
1081 // Get rid of the old savepoint and make a new one.
1082 $this->redo_savepoint();
1089 function last_error() {
1091 * Return string describing the last error on the connection.
1096 * String describing last error on the connection.
1099 * $last_pg_error Referenced.
1104 if (isset($this->last_pg_error)) {
1105 return $this->last_pg_error;
1108 return pg_last_error($this->pg);
1111 function sloppy_rollback() {
1113 * Private function, for internal use only.
1115 * Rollback the current (sloppy) transaction.
1122 * $last_pg_error Cleared.
1125 * Rolls back the current transaction.
1128 * Called only when in a sloppy transaction that has been continued
1129 * and has 'completed' without further error.
1133 // The sloppy transaction had a failure.
1134 // Rollback the 'continued' transaction.
1135 if (pg_query($this->pg, 'ROLLBACK;') === FALSE) {
1136 error_log('The sloppy transaction could not be rolled back');
1137 // This is kinda bad, we don't want to commit things that we shouldn't.
1138 exit('Internal error: Failed to rollback a sloppy transaction');
1141 unset($this->last_pg_error);
1147 * Commit the current transaction.
1152 * TRUE on success or FALSE on failure.
1155 * $last_pg_error Cleared.
1158 * Completes the current transaction.
1163 if ($this->sloppy && $this->faulting) {
1164 // The sloppy transaction had a failure.
1165 $this->sloppy_rollback();
1166 return FALSE; // The transaction did not commit.
1169 unset($this->last_pg_error);
1171 if (pg_query($this->pg, 'COMMIT;') === FALSE) {
1178 function complete() {
1180 * Commit the current transaction, if possible.
1185 * TRUE if the transaction completed or FALSE if it did not.
1188 * $last_pg_error Cleared.
1191 * Completes the current transaction.
1194 * If the transaction won't commit, roll it back instead.
1197 if ($this->sloppy && $this->faulting) {
1198 // The sloppy transaction had a failure.
1199 $this->sloppy_rollback();
1200 return FALSE; // The sloppy transaction did not commit.
1203 if (!$this->commit()) {
1204 $this->rollback('<p>Failed to commit transaction:'
1205 . '<br /><em class="error">'
1206 . htmlentities(pg_last_error($this->pg))
1211 unset($this->last_pg_error);