PHP Classes
elePHPant
Icontem

File: MyCSVAdmin/MyCSVAdmin.class.php

Recommend this page to a friend!
  Classes of Thiemo Kreuz  >  TM::MyCSV  >  MyCSVAdmin/MyCSVAdmin.class.php  >  Download  
File: MyCSVAdmin/MyCSVAdmin.class.php
Role: Class source
Content type: text/plain
Description: Administrator Main Class
Class: TM::MyCSV
Manage CSV files like database tables
Author: By
Last change:
Date: 8 years ago
Size: 38,321 bytes
 

Contents

Class file image Download
<?php

/*
 * LICENSE
 * 1. If you like to use this class for personal purposes, it's free.
 * 2. For comercial purposes, please contact me (http://maettig.com/email).
 *    I'll send a license to you.
 * 3. When you copy the framework you must copy this notice with the source
 *    code. You may alter the source code, but you have to put the original
 *    with your altered version.
 * 4. The license is for all files included in this bundle.
 *
 * KNOWN BUGS/TODO
 * - Catch \x00 when uploading a file!
 * - Use PHP types only (null/bool/int/float/string)! Add "attribute": "signed",
 *   "unsigned", "binary".
 * - Table files aren't portable to older versions or external software when
 *   using PHP >=4.3.2 because fgetcsv() needs escaped backslashes then.
 */

define("PRIV_INSERT",    2);
define("PRIV_UPDATE",    4);
define("PRIV_DELETE",    8);
define("PRIV_CREATE",   16);
define("PRIV_DROP",     32);
define("PRIV_ALTER",   512);
define("PRIV_EXPORT", 1024);
define("PRIV_ALL",      -1);

/**
 * Universal administration tool and demo application for the TM::MyCSV class.
 *
 * Don't hesitate to
 * <a href="http://bugs.maettig.com/">report bugs or feature requests</a>.
 *
 * @author Thiemo Mättig (http://maettig.com/)
 * @version 2004-12-18
 * @package TM
 */
class MyCSVAdmin
{
    /**
     * @var string Represents the $_SERVER['PHP_SELF'] value.
     */
    var $SELF = "./";

    /**
     * @var mixed Relative path(s) where the .csv and .txt files are stored.
     */
    var $dir = "./";

    /**
     * Add the values in the following table to grand or deny access to specific
     * functions of the administrative interface. Default is PRIV_ALL,
     * everything enabled.
     *
     * - PRIV_INSERT - Insert rows
     * - PRIV_UPDATE - Update rows
     * - PRIV_DELETE - Delete rows
     * - PRIV_CREATE - Create tables
     * - PRIV_DROP - Drop tables
     * - PRIV_ALTER - Alter tables
     * - PRIV_EXPORT - Export tables</pre>
     *
     * Example:
     *
     * <pre>$admin->priv = PRIV_ALL & ~PRIV_CREATE & ~PRIV_DROP & ~PRIV_ALTER;</pre>
     *
     * @var int Privileges to grand or deny access to specific functions.
     */
    var $priv = PRIV_ALL;

    /*
     * @var string Defaults to "ISO-8859-1".
     */
    var $charset = "ISO-8859-1";

    /**
     * @var MyCSV MyCSV object currently edited by TM::MyCSVAdmin.
     */
    var $table = null;

    /**
     * @var string Contains the suggested field types for the table.
     */
    var $types = array();

    /**
     * Initializes the class. Loads the table specified by $_GET['table'] or
     * $_POST['table']. Returns a new MyCSVAdmin object.
     *
     * @param string $dir
     * @return MyCSVAdmin
     */
    function MyCSVAdmin($dir = null)
    {
        if (isset($dir)) $this->dir = $dir;

        if (isset($_SERVER['PHP_SELF'])) $this->SELF = $_SERVER['PHP_SELF'];
        elseif (isset($GLOBALS['PHP_SELF'])) $this->SELF = $GLOBALS['PHP_SELF'];
        $this->SELF = str_replace("index.php", "", $this->SELF);

        if ($this->GET('table'))
        {
            $this->table = new MyCSV($this->GET('table'));
            $this->_getTypes();
        }
    }

    /**
     * Gets a GET or POST value. This unifies the different behaviours of $_GET,
     * $HTTP_GET_VARS, $GLOBALS (register_globals) and so on.
     *
     * @param string $key Name of the request variable to be returned.
     * @return string
     */
    function GET($key)
    {
        if (isset($_GET[$key])) $value = $_GET[$key];
        elseif (isset($GLOBALS['HTTP_GET_VARS'][$key])) $value = $GLOBALS['HTTP_GET_VARS'][$key];
        elseif (isset($_POST[$key])) $value = $_POST[$key];
        elseif (isset($GLOBALS['HTTP_POST_VARS'][$key])) $value = $GLOBALS['HTTP_POST_VARS'][$key];
        else return "";
        if (get_magic_quotes_gpc()) return stripslashes($value);
        return $value;
    }

    /**
     * Makes a string well readable to the browser. It strips any critical
     * characters, truncates the string and replaces HTML entities.
     *
     * @param string $text Text to be HTMLed.
     * @return string
     */
    function htmlQuote($text)
    {
        if (strlen($text) > 100) $text = substr($text, 0, 80) . "...";
        $text = preg_replace(
            '/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F\x81\x83\x86-\x8F\x90\x98-\x9F]/',
            '.', $text);
        $text = wordwrap($text, 20, "\n", true);
        return htmlentities($text, ENT_NOQUOTES, $this->charset);
    }

    /**
     * Tries to guess the field types based on what values are in the table
     * rows.
     *
     * @return void
     * @access private
     */
    function _getTypes()
    {
        $types = array(0 => "null", 1 => "bool", 2 => "int", 3 => "float",
            4 => "varchar", 5 => "text", 6 => "blob");
        $tLeast = $this->table->count() < 2 ? 4 : 0;
        if ($this->GET('override')) { $types[1] = "int"; $types[4] = "text"; $types[6] = "text"; }
        foreach ($this->table->fields as $field)
        {
            $t[$field] = strcasecmp($field, "id") ? $tLeast : 2;
        }
        while ($row = $this->table->each())
        {
            foreach ($row as $field => $value)
            {
                if (preg_match('/[\x00-\x06\x08\x0B-\x0C\x0E-\x13\x16-\x1F]/', $value))
                    $t[$field] = 6;
                elseif (strlen($value) > 255 || strpos($value, "\n"))
                    $t[$field] = max($t[$field], 5);
                elseif (! empty($value) && ! is_numeric($value))
                    $t[$field] = max($t[$field], 4);
                elseif (! preg_match('/^[+-]?\d{0,10}$/s', $value))
                    $t[$field] = max($t[$field], 3);
                elseif (! preg_match('/^[01]?$/s', $value))
                    $t[$field] = max($t[$field], 2);
                elseif (strlen($value))
                    $t[$field] = max($t[$field], 1);
            }
        }
        foreach ($this->table->fields as $field)
        {
            $this->types[$field] = $types[$t[$field]];
        }
        $this->table->reset();
    }

    /**
     * @param data string
     * @return string
     * @access private
     */
    function _mime_content_type($data)
    {
        // See magic.mime
        if (substr($data, 0, 2) == "\xFF\xD8") return 'image/jpeg';
        elseif (substr($data, 1, 3) == 'PNG') return 'image/png';
        elseif (substr($data, 0, 4) == 'GIF8') return 'image/gif';
        else return 'application/octet-stream';
    }

    /**
     * @return void
     * @access private
     */
    function displayHead()
    {
        echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
        echo '<html lang="en">';
        echo '<head>';
        echo '<meta http-equiv="content-type" content="text/html; charset=' . $this->charset . '">';
        echo '<title>TM::MyCSVAdmin';
        if (isset($this->table))
        {
            $table = $this->table->tablename();
            echo ' - Table ' . htmlspecialchars(basename($table));
        }
        echo '</title>';
        echo '<meta name="robots" content="noindex,nofollow">';
        echo '<style type="text/css">';
        echo 'h1{font-size:150%;margin-top:0;}';
        echo '.subtitle{font-size:x-small;font-weight:normal;}';
        echo 'ul#menu{border-bottom:2px solid #CFE8CF;margin-left:0;padding:0;}';
        echo 'ul#menu li{display:inline;}';
        echo 'ul#menu li a{background:#F0F7F0;border:solid #CFE8CF;border-width:1px 1px 0 1px;margin-left:10px;padding:0 10px;text-align:center;width:70px;}';
        echo 'th{background-color:#CFE8CF;}';
        echo 'td{background-color:#F0F7F0;vertical-align:top;}';
        echo 'small{color:#666;}';
        echo 'acronym{border-bottom:1px dotted #666;}';
        echo '.primary{color:#960;font-weight:bold;text-decoration:underline;}';
        echo '.error{color:#C00;}';
        echo '.danger:hover,#menu .danger:hover{background-color:#C30;color:white;text-decoration:none;}';
        echo '.download,.override{text-decoration:none;}';
        echo 'a:hover{color:#C30;text-decoration:underline;}';
        echo '#menu a:hover{background-color:#CFE8CF;}';
        echo 'tr:hover td{background-color:#CFE8CF;}';
        echo '</style>';
        if (file_exists("MyCSVAdmin.css"))
        {
            echo '<link rel="stylesheet" type="text/css" href="MyCSVAdmin.css">';
        }
        echo '</head>';
        echo "<body bgcolor=\"white\" text=\"black\" link=\"#006600\" alink=\"#CC3300\" vlink=\"#006600\">\n\n";

        $script = preg_replace('{[^/]+(/index)?\.\w+$}', '', getenv('SCRIPT_NAME'));
        echo '<a class="subtitle" href="' . $script . '">Go to parrent site</a>';
        echo '<h1>TM::<a href="' . $this->SELF . '">MyCSVAdmin</a>';
        if (isset($this->table))
        {
            echo ' &ndash; Table <a href="' . $this->SELF . '?method=structure&table=' . htmlspecialchars($table) . '">';
            echo '<em>' . htmlspecialchars(basename($table)) . '</em></a>';
        }
        echo '</h1>';

        if (isset($this->table))
        {
            echo '<ul id="menu">';
            echo '<li><a href="' . $this->SELF . '?method=browse&table=' . $table . '">Browse</a></li>';
            if ($this->priv & PRIV_INSERT)
                echo '<li><a href="' . $this->SELF . '?method=change&table=' . $table . '">Insert</a></li>';
            echo '<li><a href="' . $this->SELF . '?method=structure&table=' . $table . '">Structure</a></li>';
            if ($this->priv & PRIV_EXPORT)
                echo '<li><a href="' . $this->SELF . '?method=export&table=' . $table . '">Export</a></li>';
            if ($this->priv & PRIV_DROP)
                echo '<li><a class="danger" href="' . $this->SELF . '?method=delete_all&table=' . $table . '">Empty</a></li>';
            if ($this->priv & PRIV_DROP)
                echo '<li><a class="danger" href="' . $this->SELF . '?method=drop_table&table=' . $table . '">Drop</a></li>';
            echo '</ul>';
        }
    }

    /**
     * Shows a list of all tables. Use {@link dir} to specify the working
     * directory.
     *
     * @return void
     */
    function tables()
    {
        $form = new Apeform(0, 0, false);
        $table = $form->text("<u>T</u>able name");
        if (! preg_match('{[/\\\]}', $table))
        {
            $dir = (array)$this->dir;
            if (! preg_match('{[/\\\]$}', $dir[0])) $dir[0] .= "/";
            $table = $dir[0] . $table;
        }
        $form->submit("Create new table");
        if ($form->isValid())
        {
            $this->table = new MyCSV($table);
            // Jump to the table structure.
            $this->structure();
            return;
        }
        
        $tables = array();
        foreach ((array)$this->dir as $dir)
        {
            if (! preg_match('{[/\\\]$}', $dir)) $dir .= "/";
            $fp = opendir($dir);
            while (($filename = readdir($fp)) !== false)
            {
                $filename = $dir . $filename;
                if (! is_file($filename)) continue;
                if (! preg_match('/\.([a-z]sv|txt)$/i', $filename)) continue;
                $tables[] = $filename;
            }
            closedir($fp);
        }
        usort($tables, 'strcasecmp');

        $this->displayHead();

        echo '<table>';
        echo '<tr><th>Table</th><th colspan="6">Action</th><th>Rows</th><th>Size</th></tr>';
        $count = 0;
        $sumRecords = 0;
        $sumSize = 0;
        foreach ($tables as $filename)
        {
            $csv = new MyCSV($filename);
            $table = $csv->tablename();
            $count++;
            $sumRecords += $records = $csv->num_rows();
            $sumSize += $size = filesize($filename);
            $csv->close();

            echo '<tr><td>' . htmlspecialchars(basename($table)) . '</td>';
            echo '<td><a href="' . $this->SELF . '?method=browse&table=' . $table . '">Browse</a></td>';
            echo '<td>';
            if ($this->priv & PRIV_INSERT)
                echo '<a href="' . $this->SELF . '?method=change&table=' . $table . '">Insert</a>';
            echo '</td>';
            echo '<td><a href="' . $this->SELF . '?method=structure&table=' . $table . '">Structure</a></td>';
            echo '<td>';
            if ($this->priv & PRIV_EXPORT)
                echo '<a href="' . $this->SELF . '?method=export&table=' . $table . '">Export</a>';
            echo '</td><td>';
            if ($this->priv & PRIV_DROP)
                echo '<a class="danger" href="' . $this->SELF . '?method=delete_all&table=' . $table . '">Empty</a>';
            echo '</td><td>';
            if ($this->priv & PRIV_DROP)
                echo '<a class="danger" href="' . $this->SELF . '?method=drop_table&table=' . $table . '">Drop</a>';
            echo '</td>';
            echo '<td align="right">' . number_format($records) . '</td>';
            echo '<td align="right">' . number_format($size / 1024, 1) . ' KB</td>';
            echo '</tr>';
        }
        echo '<tr>';
        echo '<th>' . $count . ' tables</th>';
        echo '<th colspan="6">Sum</th>';
        echo '<th align="right">' . number_format($sumRecords) . '</th>';
        echo '<th align="right">' . number_format($sumSize / 1024, 1) . ' KB</th>';
        echo '</tr>';
        echo '</table>';

        if ($this->priv & PRIV_CREATE) $form->display();
    }
    
    /**
     * Shows the structure of a table.
     *
     * @return void
     */
    function structure()
    {
        $form = new Apeform(0, 0, false);
        $form->hidden("structure", "method");
        $form->hidden($this->table->tablename(), "table");
        $field = &$form->hidden($this->GET('method') == "structure" ? $this->GET('field') : "");
        $newField = &$form->text("<u>F</u>ield name", "", $field);

        foreach ($this->table->fields as $key) $positions[$key] = "After " . $key;
        $keys = array_keys($positions);
        $defaultValue = ($field && in_array($field, $keys)) ? $keys[array_search($field, $keys) - 1] : "";
        if ($field) unset($positions[$field]);
        array_pop($positions);
        $positions[''] = "At end of table";
        $label = $field ? "Move to <u>p</u>osition" : "Add at <u>p</u>osition";
        $afterField = $form->select($label, "", $positions, $defaultValue);

        $button = $field ? "Rename/move field" : "Add new field";
        $form->submit($button);

        if ($form->isValid())
        {
            if (! $field)
            {
                if ($this->table->add_field($newField, $afterField)) $this->table->write();
                else $form->error("Invalid field name", 3);
            }
            else
            {
                while ($row = $this->table->each())
                {
                    // Copy all the moved values to their new field name.
                    $this->table->data[$row['id']][$newField] = $row[$field];
                }
                $newFields = array();
                foreach ($this->table->fields as $oldField)
                {
                    // Copy any unchanged field to the new fields array.
                    if ($oldField != $field) $newFields[] = $oldField;
                    if ($oldField == $afterField) $newFields[] = $newField;
                }
                // Add field if position '' ("At end of table") was selected.
                if (! $afterField) $newFields[] = $newField;
                $this->table->fields = $newFields;
                $this->table->write();
            }
            $field = "";
            $newField = "";
        }

        $formOrder = new Apeform(0, 0, "order", false);
        $formOrder->hidden("structure", "method");
        $formOrder->hidden($this->table->tablename(), "table");
        $order = $formOrder->select("Permanently <u>o</u>rder table by", "",
            $this->table->fields);
        $formOrder->submit("Order");

        if ($formOrder->isValid())
        {
            $this->table->sort($this->table->fields[$order]);
            $this->table->write();
        }

        $this->displayHead();

        echo '<table>';
        echo '<tr><th>Field</th><th><small>Guessed type</small></th><th colspan="2">Action</th></tr>';
        foreach ($this->table->fields as $i => $f)
        {
            echo '<tr><td';
            if (! $i) echo ' class="primary" title="Row identifier"';
            echo '>' . $f . '</td>';
            echo '<td><small>' . (isset($this->types[$f]) ? $this->types[$f] : "") . '</small></td>';
            echo '<td>';
            if ($i && $this->priv & PRIV_ALTER)
                echo '<a href="' . $this->SELF . '?method=structure&table=' . $this->table->tablename() . '&field=' . $f . '">Change</a>';
            echo '</td><td>';
            if ($i && $this->priv & PRIV_ALTER)
                echo '<a class="danger" href="' . $this->SELF . '?method=drop&table=' . $this->table->tablename() . '&field=' . $f . '">Drop</a>';
            echo '</td></tr>';
        }
        echo '</table>';

        if ($this->priv & PRIV_ALTER) $form->display();
        if ($this->priv & PRIV_UPDATE) $formOrder->display();

        echo '<p><table>';
        echo '<tr><th>Statement</th><th>Value</th></tr>';
        echo '<tr><td>Rows</td><td align="right">' . $this->table->num_rows() . '</td></tr>';
        echo '<tr><td>Next autoindex</td><td align="right">' . ($this->table->num_rows() ? max($this->table->ids()) + 1 : 1) . '</td></tr>';
        $filesize = $this->table->exists() ? filesize($this->table->filename) : 0;
        $filemtime = $this->table->exists() ? date("Y-m-d H:i", filemtime($this->table->filename)) : "";
        echo '<tr><td>File size</td><td align="right">' . number_format($filesize) . ' Bytes</td></tr>';
        $size = $this->table->num_rows()
            ? (int)round(($filesize - strlen(implode(",", $this->table->fields)) - 2) / $this->table->num_rows())
            : 0;
        echo '<tr><td>Average row size</td><td align="right">' . $size . ' Bytes</td></tr>';
        echo '<tr><td>Delimiter</td><td align="right">' . $this->table->delimiter . '</td></tr>';
        echo '<tr><td>Last update</td><td align="right">' . $filemtime . '</td></tr>';
        echo '</table>';
    }

    /**
     * Shows the contents of a table.
     *
     * @return void
     */
    function browse()
    {
        $sort = $this->GET('sort');
        if ($sort) $this->table->sort($sort . " SORT_NULL");
        $rows = (int)$this->GET('rows');
        if (empty($rows) || $rows < 1) $rows = 50;

        if ($this->table->count() > $rows)
        {
            $id = $this->GET('id');
            if (empty($id)) $id = $this->table->first();
            $this->table->limit($rows, $id);

            $form = '<form action="' . $this->SELF . '" method="get"><p>';
            $aHref = '<a href="' . $this->SELF . '?method=browse&amp;table=' . $this->table->tablename() . '&amp;sort=' . urlencode($sort) . '&amp;rows=' . $rows . '&amp;id=';
            $first = $this->table->first();
            $prev  = $this->table->prev($id, $rows);
            $next  = $this->table->next($id, $rows);
            $last  = $this->table->prev($this->table->last(), ($this->table->count() - 1) % $rows);
            if (strcmp($first, $id) == 0) $form .= '&lt;&lt; First | ';
            else $form .= $aHref . urlencode($first) . '">&lt;&lt; First</a> | ';
            if ($prev === false) $form .= '&lt; Previous | ';
            else $form .= $aHref . urlencode($prev) . '">&lt; Previous</a> | ';
            $form .= '<input name="method" type="hidden" value="browse">';
            $form .= '<input name="table" type="hidden" value="' . $this->table->tablename() . '">';
            $form .= '<input name="sort" type="hidden" value="' . htmlspecialchars($sort) . '">';
            $form .= '<input type="submit" value="Show"> ';
            $form .= '<input name="rows" size="4" type="text" value="' . $rows . '"> rows starting from <span class="primary" title="Row identifier">id</span> ';
            $form .= '<input name="id" size="12" type="text" value="' . htmlspecialchars($id) . '"> | ';
            if ($next === false) $form .= 'Next &gt; | ';
            else $form .= $aHref . urlencode($next) . '">Next &gt;</a> | ';
            if (strcmp($last, $id) == 0) $form .= 'Last &gt;&gt;';
            else $form .= $aHref . urlencode($last) . '">Last &gt;&gt;</a>';
            $form .= '</p></form>';
        }

        $this->displayHead();
        if (! empty($form)) echo $form;

        echo '<table>';
        echo '<tr><th colspan="2"></th>';
        foreach ($this->table->fields as $i => $field)
        {
            $field = $this->htmlQuote($field);
            echo '<th><a href="' . $this->SELF;
            echo '?method=browse&amp;table=' . $this->table->tablename() . '&amp;sort=' . urlencode($field);
            if ($sort == $field) echo urlencode(' DESC');
            echo '&amp;rows=' . urlencode($rows);
            echo '" title="Order"';
            if (! $i) echo ' class="primary"';
            echo '>' . $field . '</a>';
            if (strcmp($sort, $field) == 0) echo '+';
            elseif (substr($sort, 0, -5) == $field) echo '-';
            echo '</th>';
        }
        echo '</tr>';

        while ($row = $this->table->each())
        {
            echo '<tr><td>';
            if ($this->priv & PRIV_UPDATE)
                echo '<a href="' . $this->SELF . '?method=change&table=' . $this->table->tablename() . '&id=' . urlencode($row['id']) . '">Edit</a>';
            echo '</td><td>';
            if ($this->priv & PRIV_DELETE)
                echo '<a class="danger" href="' . $this->SELF . '?method=delete&table=' . $this->table->tablename() . '&id=' . urlencode($row['id']) . '">Delete</a>';
            echo '</td>';
            foreach ($this->table->fields as $field)
            {
                if (! isset($row[$field])) $row[$field] = "";
                switch ($this->types[$field])
                {
                    case "bool":
                        echo '<td align="center">' . ($row[$field] ? '&times;' : '') . '</td>';
                        break;
                    case "int":
                    case "float":
                        echo '<td align="right">' . $row[$field] . '</td>';
                        break;
                    case "blob":
                        echo '<td><a href="' . $this->SELF;
                        echo '?method=download&amp;table=' . $this->table->tablename();
                        echo '&amp;id=' . urlencode($row['id']) . '&amp;field=' . $field . '"';
                        echo ' class="download" title="Download binary file">';
                        echo $this->htmlQuote($row[$field]) . '</a></td>';
                        break;
                    default:
                        echo '<td>' . $this->htmlQuote($row[$field]) . '</td>';
                }
            }
            echo '</tr>';
        }
        echo '</table>';

        if (! empty($form)) echo $form;
        if ($this->priv & PRIV_INSERT)
            echo '<p><a href="' . $this->SELF . '?method=change&table=' . $this->table->tablename() . '">Insert new row</a></p>';
    }
    
    /**
     * Shows a form to insert or update a row.
     *
     * @return void
     */
    function change()
    {
        $form = new Apeform(10000, 60, false);
        $form->hidden("change", "method");
        $table = $form->hidden($this->table->tablename(), "table");
        $id = &$form->hidden($this->GET('id'), "id");
        $form->hidden($this->GET('override'), "override");
        $row = $this->table->data($id);

        $function = basename($this->table->tablename()) . "_onRowLoaded";
        if (function_exists($function))
        {
            $function(&$row, &$this->table);
        }

        foreach ($this->table->fields as $i => $field)
        {
            $label = $i ? $field : ('<span class="primary" title="Row identifier">' . $field . '</span>');
            $help = "";
            if (! $this->GET('override'))
            {
                if ($field == "id") $help = "Primary key";
                elseif ($this->types[$field] == "null") $help = "";
                else $help = "Guessed type: " . $this->types[$field];
                if ($this->types[$field] == "bool" || $this->types[$field] == "varchar" || $this->types[$field] == "blob")
                {
                    $help .= ' (<a class="override" href="' . $this->SELF . '?method=change';
                    $help .= '&table=' . $table . '&id=' . $id . '&override=1">override</a>)';
                }
            }
            $value = isset($row[$field]) ? $row[$field] : "";

            $function = basename($this->table->tablename()) . "_" . $field .
                "_onDisplay";

            if (! $this->GET('override') && preg_match('/^(.+)_id$/i', $field, $matches))
            {
                $tablename = dirname($this->table->tablename());
                if ($tablename) $tablename .= "/";
                $tablename .= $matches[1];
                $foreignTable = new MyCSV($tablename);
                if ($foreignTable->exists())
                {
                    $fField = $foreignTable->fields[1];
                    $foreignTable->sort($fField);
                    $options = array();
                    while ($fRow = $foreignTable->each())
                    {
                        $options[$fRow['id']] = substr($fRow['id'] . " (" . $fRow[$fField], 0, $form->size - 4) . ")";
                    }
                    $help = 'Foreign key (<a class="override" href="' . $this->SELF . '?method=change';
                    $help .= '&table=' . $table . '&id=' . $id . '&override=true">override</a>)';
                    $changedRow[$field] = $form->select($label, $help, $options, $value);
                    $foreignTable->close();

                    if (function_exists($function))
                    {
                        $function(&$form->_rows[count($form->_rows) - 1], &$row);
                    }
                    continue;
                }
            }

            switch ($this->types[$field])
            {
                case "bool":
                    $changedRow[$field] = $form->checkbox($label, $help,
                        array(1 => ""), $value);
                    break;
                case "int":
                case "float":
                    $changedRow[$field] = $form->text($label, $help, $value, 0,
                        10);
                    break;
                case "text":
                    $changedRow[$field] = $form->textarea($label, $help, $value,
                        7);
                    break;
                case "blob":
                    if ($file = $form->file($label, $help))
                    {
                        $fp = fopen($file['tmp_name'], "rb");
                        $changedRow[$field] = fread($fp, 10000);
                        fclose($fp);
                    }
                    break;
                default:
                    $changedRow[$field] = $form->text($label, $help, $value);
            }

            if (function_exists($function))
            {
                $function(&$form->_rows[count($form->_rows) - 1], &$row);
            }
        }
        // Show these both Buttons in edit/update() mode.
        $button = array();
        if ($this->priv & PRIV_UPDATE) $button[] = "Save changes";
        if ($this->priv & PRIV_INSERT) $button[] = "Save as new row";
        if ($this->priv & PRIV_DELETE) $button[] = "Delete";
        // Show this Button in insert() mode.
        if (empty($changedRow['id'])) $button = "Insert new row";
        $button = $form->submit($button);

        if ($form->isValid() && strstr($button, " new row") &&
            $this->table->id_exists($changedRow['id']))
        {
            $form->error('Duplicate entry for <span class="primary">id</span>',
                4);
        }
        if ($form->isValid())
        {
            if ($button == "Delete") { $this->delete(); return; }

            $function = basename($this->table->tablename()) . "_onRowValidated";
            if (function_exists($function))
            {
                $function(&$changedRow, &$this->table);
            }

            if ($button == "Save as new row") $id = "";
            if ($id != "") $this->table->update($changedRow, $id);
            else $this->table->insert($changedRow);

            $function = basename($this->table->tablename()) . "_onRowUpdated";
            if (function_exists($function))
            {
                $function(&$this->table);
            }

            $this->table->write();
            $id = $changedRow['id'];
            if ($id == "") { $this->browse(); return; }
        }

        $this->displayHead();

        $form->display();
    
        if ($id != "")
        {
            echo '<p>';
            $aHref = '<a href="' . $this->SELF . '?method=change&table=' . $table;
            if ($this->table->prev($id) === false)
                echo '&lt;&lt; First | &lt; Previous | ';
            else
            {
                echo $aHref . '&id=' . urlencode($this->table->first()) . '">&lt;&lt; First</a> | ';
                echo $aHref . '&id=' . urlencode($this->table->prev($id)) . '">&lt; Previous</a> | ';
            }
            if ($this->table->next($id) === false)
                echo 'Next &gt; | Last &gt;&gt;';
            else
            {
                echo $aHref . '&id=' . urlencode($this->table->next($id)) . '">Next &gt;</a> | ';
                echo $aHref . '&id=' . urlencode($this->table->last()) . '">Last &gt;&gt;</a>';
            }
            echo '</p>';
        }
    }

    /**
     * Shows a form to export the table.
     *
     * @return void
     */
    function export()
    {
        $form = new Apeform(0, 0, false);
        $form->hidden("export", "method");
        $table = $form->hidden($this->table->tablename(), "table");
        $options = array(
            'csv' => '<acronym title="Comma Separated Values"><u>C</u>SV</acronym>/TXT',
            'sql' => '<acronym title="Structured Query Language"><u>S</u>QL</acronym> dump',
            'xml' => '<acronym title="Extensible Markup Language"><u>X</u>ML</acronym> <span style="font-size:smaller">(<a href="http://www.google.de/search?q=FlatXmlDataSet">FlatXmlDataSet</a>)</span>');
        $type = $form->radio("Export as", "", $options, "csv");
        $options = array(
            "," => "Comma (,)",
            ";" => "Semikolon (;)",
            "\t" => "Tabulator",
            "\\0" => "Nul",
            "|" => "Pipe (|)",
            "&" => "Ampersand (&amp;)",
            ":" => "Colon (:)",
            " " => "Space");
        $delimiter = $form->select("CSV/TXT <u>d</u>elimiter", "", $options, ",");
        if ($delimiter == "\\0") $delimiter = chr(0);
        $sections = $form->checkbox("SQL sections", "", "Structure|Data", "Structure|Data");
        $save = $form->checkbox("Save as <u>f</u>ile");
        if (function_exists("gzencode")) $compress = $form->checkbox("Save <u>g</u>zip compressed");
        $form->submit("Export");

        $tablename = preg_replace('/\W+/', '_', basename($this->table->tablename()));

        if ($form->isValid() && $type == "csv")
        {
            $this->table->delimiter = $delimiter;
            $export = $this->table->export();
        }
        elseif ($form->isValid() && $type == "sql")
        {
            $export = "CREATE TABLE `" . $tablename . "` (\n";
            foreach ($this->table->fields as $field)
            {
                $export .= "  `" . preg_replace('/\W+/', '_', $field) . "` ";
                if ($this->types[$field] == "varchar" || $this->types[$field] == "null") $export .= "VARCHAR(255)";
                elseif ($this->types[$field] == "bool") $export .= "TINYINT";
                else $export .= strtoupper($this->types[$field]);
                $export .= " NOT NULL";
                if ($field == "id") $export .= " AUTO_INCREMENT";
                $export .= ",\n";
            }
            $export .= "  PRIMARY KEY (`id`)\n);\n\n";
            if (! in_array("Structure", $sections)) $export = "";
            if (! in_array("Data", $sections)) $this->table->delete();
            while ($row = $this->table->each())
            {
                foreach ($row as $field => $value)
                {
                    $sqlRow[preg_replace('/\W+/', '_', $field)] = "'" . addslashes($value) . "'";
                }
                $export .= "INSERT INTO `" . $tablename;
                $export .= "` (`" . implode("`, `", array_keys($sqlRow)) . "`) VALUES (" . implode(", ", $sqlRow) . ");\n";
            }
        }
        elseif ($form->isValid() && $type == "xml")
        {
            $export = '<?xml version="1.0" encoding="' . $this->charset . "\"?>\n";
            $export .= "<dataset>\n";
            while ($row = $this->table->each())
            {
                $export .= '  <' . $tablename . ' id="' . $row['id'] . '"';
                unset($row['id']);
                foreach ($row as $field => $value)
                {
                    $field = preg_replace('/[^a-z0-9-]+/i', '_', $field);
                    if (! preg_match('/^[a-z]/i', $field)) $field = "field" . $field;
                    $export .= ' ' . $field . '="' . htmlspecialchars($value) . '"';
                }
                $export .= " />\n";
            }
            $export .= "</dataset>\n";
        }

        if ($save)
        {
            $mimeType = 'text/comma-separated-values';
            $extension = "." . $type;
            if ($type == "csv" && $delimiter == "\t")
            {
                $mimeType = 'text/tab-separated-values';
                $extension = ".txt";
            }
            elseif ($type == "sql") $mimeType = 'application/octet-stream';
            elseif ($type == "xml") $mimeType = 'text/xml';
            if (isset($compress) && $compress)
            {
                $export = gzencode($export);
                $mimeType = 'application/x-gzip';
                $extension .= ".gz";
            }
            $filename = basename($this->table->tablename()) . $extension;
            header('Content-Type: ' . $mimeType);
            header('Content-Length: ' . strlen($export));
            header('Content-Disposition: attachement; filename="' . $filename . '";');
            echo $export;
            return;
        }

        $this->displayHead();

        $form->display();

        echo "<pre>";
        if (isset($export)) echo htmlentities($export, ENT_NOQUOTES, $this->charset);
    }

    /**
     * Download binary file specified by tablename, id and field.
     *
     * @return void
     */
    function download()
    {
        $field = $this->GET('field');
        $id = $this->GET('id');
        $file = $this->table->data[$id][$field];
        header('Content-Type: ' . $this->_mime_content_type($file));
        header('Content-Length: ' . strlen($file));
        header('Content-Disposition: attachment; filename="' . $field . '_' . $id . '";');
        echo $file;
    }

    /**
     * Confirms the deletion of a row.
     *
     * @return void
     */
    function delete()
    {
        if ($this->GET('sure'))
        {
            $function = basename($this->table->tablename()) . "_onRowDelete";
            if (function_exists($function))
            {
                $function($this->table->data($this->GET('id')), &$this->table);
            }

            $this->table->delete($this->GET('id'));
            $this->table->write();
            $this->browse();
            return;
        }
        $this->displayHead();
        echo 'Do you really want to<br>delete row \'' . $this->GET('id') . '\'?<p>';
        echo '<a class="danger" href="' . $this->SELF . '?method=delete&table=' . $this->table->tablename() . '&id=' . $this->GET('id') . '&sure=1">Yes</a> | ';
        echo '<a href="' . $this->SELF . '?method=browse&table=' . $this->table->tablename() . '">No</a>';
    }

    /**
     * Confirms the deletion of all rows.
     *
     * @return void
     */
    function delete_all()
    {
        if ($this->GET('sure'))
        {
            $this->table->delete();
            $this->table->write();
            $this->structure();
            return;
        }
        $this->displayHead();
        echo 'Do you really want to<br>delete all rows from table `' . $this->table->tablename() . '`?<p>';
        echo '<a class="danger" href="' . $this->SELF . '?method=delete_all&table=' . $this->table->tablename() . '&sure=1">Yes</a> | ';
        echo '<a href="' . $this->SELF . '?method=browse&table=' . $this->table->tablename() . '">No</a>';
    }
    
    /**
     * Confirms the deletion of a column.
     *
     * @return void
     */
    function drop()
    {
        if ($this->GET('sure'))
        {
            $this->table->drop_field($this->GET('field'));
            $this->table->write();
            $this->structure();
            return;
        }
        $this->displayHead();
        echo 'Do you really want to<br>drop field `' . $this->GET('field') . '`?<p>';
        echo '<a class="danger" href="' . $this->SELF . '?method=drop&table=' . $this->table->tablename() . '&field=' . $this->GET('field') . '&sure=1">Yes</a> | ';
        echo '<a href="' . $this->SELF . '?method=structure&table=' . $this->table->tablename() . '">No</a>';
    }

    /**
     * Confirms the deletion of a table.
     *
     * @return void
     */
    function drop_table()
    {
        if ($this->GET('sure'))
        {
            $this->table->drop_table();
            $this->table->write();
            unset($this->table);
            $this->tables();
            return;
        }
        $this->displayHead();
        echo 'Do you really want to<br>drop table `' . $this->table->tablename() . '`?<p>';
        echo '<a class="danger" href="' . $this->SELF . '?method=drop_table&table=' . $this->table->tablename() . '&sure=1">Yes</a> | ';
        echo '<a href="' . $this->SELF . '?method=browse&table=' . $this->table->tablename() . '">No</a>';
    }
}

?>