<?PHP
#
#   FILE:  installcwis.php
#
#   Part of the Collection Workflow Integration System
#   Copyright 2009 Edward Almasy and Internet Scout
#   http://scout.wisc.edu
#

// @codingStandardsIgnoreFile

# ----- CONFIGURATION --------------------------------------------------------

$G_OldestVersionWeCanUpgrade = "1.4.0";

$G_MinimumPhpVersion = "5.4.0";
$G_MinimumMysqlVersion = "5.0";

# directories that must be writable before install can begin
# (an attempt will be made to create these if they don't exist)
$G_DirsThatMustBeWritable = array(
        "/",
        "/include",
        "/tmp",
        "/local",
        "/local/logs",
        "/local/data",
        "/local/data/files",
        "/local/data/images",
        "/local/data/images/previews",
        "/local/data/images/thumbnails",
        );
# files containing SQL commands to set up initial database
# (index = file name, value = progress message)
$G_DatabaseSetupFiles = array(
        "install/TestDBPerms.sql" => "Testing database permissions...",
        "lib/AxisPHP/Session--CreateTables.sql" => "Setting up session tables...",
        "lib/AxisPHP/User--CreateTables.sql" => "Setting up user tables...",
        "lib/ScoutLib/ApplicationFramework--CreateTables.sql"
                => "Setting up application framework tables...",
        "lib/ScoutLib/SearchEngine--CreateTables.sql"
                => "Setting up search engine tables...",
        "lib/ScoutLib/EventLog--CreateTables.sql"
                => "Setting up event logging tables...",
        "lib/ScoutLib/PluginManager--CreateTables.sql"
                => "Setting up plugin manager tables...",
        "lib/ScoutLib/RSSClient--CreateTables.sql"
                => "Setting up RSS client tables...",
        "install/CreateTables.sql" => "Setting up CWIS tables...",
        );

# SQL errors we can ignore (index = SQL command, value = error message)
# (this list is mirrored in the Developer plugin)
$G_SqlErrorsWeCanIgnore = array(
        "/CREATE TABLE /i" => "/Table '[a-z0-9_]+' already exists/i",
        "/DROP TABLE /i" => "/Unknown table '[a-z0-9_]+'/i",
        "/ALTER TABLE [a-z]+ ADD PRIMARY KEY/i" => "/Multiple primary key/i",
        "/ALTER TABLE [a-z]+ ADD /i" => "/Duplicate column name/i",
        "/ALTER TABLE [a-z]+ DROP COLUMN/i" => "/check that column/i",
        "/ALTER TABLE [a-z]+ CHANGE COLUMN/i" => "/Unknown column/i",
        "/ALTER TABLE [a-z]+ ADD INDEX/i" => "/Duplicate key name/i",
        );

$G_VerbosityLevel = 1;

# minimum password length accepted by the CWIS User object
$G_MinimumPasswordLength = 6;

# ----- FORMS AND MESSAGES ---------------------------------------------------

function PrintInstallInfoForm($FVars, $ErrMsgs)
{
    # set up default values
    $Protocol = isset($_SERVER["HTTPS"]) ? "https://" : "http://";
    $SiteUrl = isset($FVars["F_SiteUrl"]) ? $FVars["F_SiteUrl"]
            : $Protocol.$_SERVER["HTTP_HOST"].dirname($_SERVER["SCRIPT_NAME"])."/";
    $DBHost = isset($FVars["F_DBHost"]) ? $FVars["F_DBHost"] : "localhost";
    $DBLogin = isset($FVars["F_DBLogin"]) ? $FVars["F_DBLogin"] : "";
    $DBPassword = isset($FVars["F_DBPassword"]) ? $FVars["F_DBPassword"] : "";
    $DBName = isset($FVars["F_DBName"]) ? $FVars["F_DBName"] : "";
    $AdminLogin = isset($FVars["F_AdminLogin"]) ? $FVars["F_AdminLogin"] : "";
    $AdminPassword = isset($FVars["F_AdminPassword"]) ? $FVars["F_AdminPassword"] : "";
    $AdminEmail = isset($FVars["F_AdminEmail"]) ? $FVars["F_AdminEmail"] : "";
    $AdminEmailAgain = isset($FVars["F_AdminEmailAgain"]) ? $FVars["F_AdminEmailAgain"] : "";

    # display form
    ?>
    <form method="POST" action="installcwis.php">
    <table class="InfoTable">

        <tr><td colspan="2">
        Before beginning installation, we need to gather a few bits of
        information to set up the software and allow it to access the SQL
        database where the resource data will be stored.  It is important
        that this information be entered correctly, so please take the time
        to read the notes accompanying each field to make sure you enter the
        right values, and consult your system administrator if you're not sure.
        </td></tr>

        <tr><td>&nbsp;</td></tr>
        <tr>
            <th>Site URL:</th>
            <td><input type="text" size="40" maxlength="120"
                    name="F_SiteUrl" value="<?PHP  print($SiteUrl);  ?>" /></td>
        </tr>
        <tr><td></td><td>
            This is the URL (web address) where the software will be accessed.
        </td></tr>
        <tr><td>&nbsp;</td></tr>

        <tr>
            <th>Database Host:</th>
            <td><input type="text" size="30" maxlength="60"
                    name="F_DBHost" value="<?PHP  print($DBHost);  ?>" /></td>
        </tr>
        <tr><td></td><td>
            This is the name of the computer on which your SQL database server
            is running.  If your web server and your database server are running
            on the same computer, then you should enter <i>localhost</i> here.
        </td></tr>
        <tr><td>&nbsp;</td></tr>

        <tr>
            <th>Database Login:</th>
            <td><input type="text" size="15" maxlength="40"
                    name="F_DBLogin" value="<?PHP  print($DBLogin);  ?>" /></td>
        </tr>
        <tr>
            <th>Database Password:</th>
            <td><input type="text" size="15" maxlength="40"
                    name="F_DBPassword" value="<?PHP  print($DBPassword);  ?>" /></td>
        </tr>
        <tr><td></td><td>
            This is the login name and password that you need to
            connect to your database server.  Please note that this is a
            <b>database server</b> user name and password, which must have
            already been set up by your database administrator, <b>not</b>
            your Linux or OS X login name and password.<br />
            <br />
            Please Note:  If the database named below does not already exist,
            this database user account must have <i>CREATE</i> privileges.
            If the database <b>does</b> exist, it must not already
            contain tables or data.
        </td></tr>
        <tr><td>&nbsp;</td></tr>

        <tr>
            <th>Database Name:</th>
            <td><input type="text" size="30" maxlength="60"
                    name="F_DBName" value="<?PHP  print($DBName);  ?>" /></td>
        </tr>
        <tr><td></td><td>
            This is the name of the SQL database (the internal database name
            that you yourself choose, like <i>PortalDB</i> or <i>OurDB</i>, not
            the name of the database software package) that we will use to
            store portal information.<br />
            <br />
            Please Note: If this database already exists, it must <b>not</b>
            already contain tables or data.
        </td></tr>
        <tr><td>&nbsp;</td></tr>

        <tr>
            <th>Admin Login:</th>
            <td><input type="text" size="15" maxlength="30"
                    name="F_AdminLogin" value="<?PHP  print($AdminLogin);  ?>" /></td>
        </tr>
        <tr>
            <th>Admin Password:</th>
            <td><input type="text" size="15" maxlength="30"
                    name="F_AdminPassword" value="<?PHP  print($AdminPassword);  ?>" /></td>
        </tr>
        <tr>
            <th>Admin E-Mail:</th>
            <td><input type="text" size="40" maxlength="120"
                    name="F_AdminEmail" value="<?PHP  print($AdminEmail);  ?>" /></td>
        </tr>
        <tr>
            <th>Admin E-Mail:</th>
            <td><input type="text" size="40" maxlength="120"
                    name="F_AdminEmailAgain" value="<?PHP  print($AdminEmailAgain);  ?>" /> <b>(again to confirm)</b></td>
        </tr>
        <tr><td></td><td>
            This is the user name and password that you will initially use
            to log into and configure your portal, and the e-mail address
            where any administrative e-mail will be sent.  The password must
            be at least six characters long.
        </tr>
        <tr><td>&nbsp;</td></tr>

        <tr><td></td><td>
            <input type="submit" name="F_Submit" value="Begin Installation">
            <span style="float: right; color: grey; font-size: 12px;">
                    Verbosity: <input type="checkbox" name="F_Debug" <?PHP
                    if (isset($FVars["F_Debug"])) {  print("checked");  }
                    ?>><input type="checkbox" name="F_MoreDebug" <?PHP
                    if (isset($FVars["F_MoreDebug"])) {  print("checked");  }
                    ?>><input type="checkbox" name="F_EvenMoreDebug" <?PHP
                    if (isset($FVars["F_EvenMoreDebug"])) {  print("checked");  }
                    ?>></span>
        </tr>

    </table>
    </form>
    <?PHP
}

function PrintErrorMessages($ErrMsgs)
{
    if (count($ErrMsgs))
    {
        ?><b>Errors Encountered:</b>
        <ul class="ErrorList"><?PHP
        foreach ($ErrMsgs as $Msg)
        {
            ?><li><?PHP  print($Msg);  ?></li><?PHP
            LogMsg("ERROR: ".$Msg);
        }
        ?></ul><?PHP
    }
}

function BeginHtmlPage($OurVersion)
{
    ?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <title>CWIS <?PHP  print($OurVersion);  ?> Installation</title>
        <style type="text/css">
            body {
                background-color: #B3B3B3;
            }
            .MainTable {
                margin: 5% 10% 10% 10%;
                padding: 2em;
                background-color: #FFFFFF;
                border: 2px solid #999999;
            }
            .InfoTable {
                width: 100%;
            }
            .InfoTable tr {
                vertical-align: top;
            }
            .InfoTable th {
                text-align: right;
                white-space: nowrap;
            }
            .InstallInfoSummaryTable {
                border: 1px solid #DDDDDD;
                background-color: #EEEEEE;
                padding: 15px;
            }
            .InstallInfoSummaryTable th {
                text-align: right;
                white-space: nowrap;
                padding-right: 10px;
            }
            .TitleLine {
                font-size: 1.5em;
                font-weight: bold;
                font-family: sans-serif;
            }
            .LogoC
            {
                color: #E79D1B;
                font-weight: bold;
                font-family: sans-serif;
            }
            .LogoWIS
            {
                color: #505B84;
                font-weight: bold;
                font-family: sans-serif;
            }
            .ErrorList
            {
                color: red;
            }
        </style>
    </head>
    <body>
    <table class="MainTable">
        <tr><td colspan="2"><span class="TitleLine"><span class="LogoC">C</span><span class="LogoWIS">WIS</span> <?PHP  print($OurVersion);  ?> Installation</span></td><tr>
        <tr><td colspan="2">
        </td></tr>
        <tr><td>
    <?PHP
}

function EndHtmlPage()
{
    ?>
        </td></tr>
    </table>
    </body>
    </html>
    <?PHP
}

function PrintInstallInfoSummary($FVars, $IsUpgrade)
{
    ?>
    <table class="InstallInfoSummaryTable" width="100%">
        <tr><th>Site Base URL:</th>
                <td><i><?PHP  print($FVars["F_SiteUrl"]);  ?></i></td></tr>
        <tr><th>Database Host:</th>
                <td><i><?PHP  print($FVars["F_DBHost"]);  ?></i></td></tr>
        <tr><th>Database Login:</th>
                <td><i><?PHP  print($FVars["F_DBLogin"]);  ?></i></td></tr>
        <tr><th>Database Name:</th>
                <td><i><?PHP  print($FVars["F_DBName"]);  ?></i></td></tr>
        <?PHP
        if (!$IsUpgrade)
        {
        ?>
        <tr><th>Admin Login:</th>
                <td><i><?PHP  print($FVars["F_AdminLogin"]);  ?></i></td></tr>
        <tr><th>Admin E-Mail:</th>
                <td><i><?PHP  print($FVars["F_AdminEmail"]);  ?></i></td></tr>
        <?PHP
        }
        ?>
    </table>
    <br />
    <?PHP
}

function PrintInstallCompleteInfo($FVars, $IsUpgrade)
{
    ?>
    <br />
    <?PHP
    if ($IsUpgrade)
    {
        ?>
        You may now proceed to your <a href="<?PHP  print($FVars["F_SiteUrl"]);
            ?>index.php" target="_blank">upgraded CWIS site</a>.
            You may also want to visit and sign up for the
            <a href="https://scout.wisc.edu/cwis/forums" target="new">CWIS
            forums</a>, in case you have questions about the software or want to
            share your experience with other users.<br />
        <?PHP
        if ($FVars["F_DefaultUI"] == "SPTUI--Default")
        {
            ?>
            <br />
            <b>PLEASE NOTE:</b>  It appears that you have upgraded from SPT to CWIS.
            If some pages on your site appear to load incorrectly, you may
            need to edit the file <code>local/config.php</code> and change the
            value of <code>$GLOBALS["G_Config"]["UserInterface"]["DefaultUI"]</code>
            to <code>"default"</code>.<br />
            <?PHP
        }
    }
    else
    {
        ?>
        You may now proceed to your <a href="<?PHP  print($FVars["F_SiteUrl"]);
            ?>index.php">new CWIS site</a> and log in with the user name <i><?PHP
            print($FVars["F_AdminLogin"]);  ?></i> and the password you supplied.
            You may also want to visit and sign up for the
            <a href="https://scout.wisc.edu/cwis/forums" target="new">CWIS
            forums</a>, in case you have questions about the software or want to
            share your experience with other users.<br />
        <?PHP
    }
    ?>
    <br />
    Thank you for using <span class="LogoC">C</span><span class="LogoWIS">WIS</span>!
    <?PHP
}

function PrintHelpPointers()
{
    ?>
    <br />
    Please correct these problems and re-run the installation.<br />
    <br />
    Troubleshooting information can be found online in the
    <a href="http://scout.wisc.edu/cwis/faq"
    target="new">CWIS FAQ</a>.  Questions can be posted to the
    <a href="http://scout.wisc.edu/cwis/forums"
    target="new">CWIS Users Forums</a>.
    <?PHP
}


# ----- VALIDATION CHECKS ----------------------------------------------------

function CheckEnvironment($ErrMsgs)
{
    # check PHP version
    global $G_CwisVersion;
    global $G_MinimumPhpVersion;
    if (version_compare(PHP_VERSION, $G_MinimumPhpVersion) == -1)
    {
        $ErrMsgs[] = "Required PHP version not found."
                ." CWIS ".$G_CwisVersion." requires PHP version "
                        .$G_MinimumPhpVersion." or later."
                ."<br />PHP version <i>".PHP_VERSION
                        ."</i> was detected.";
    }

    # check for required extensions
    $RequiredExtensions = array(
        "curl",
        "gd",
        "json",
        "mysqli",
        "openssl",
        "session",
        "simplexml",
        "xml",
        "xmlreader",
        );

    foreach ($RequiredExtensions as $Ext)
    {
        if (!extension_loaded($Ext))
        {
            $ErrMsgs[] =
                 "The '".$Ext."' extension is not loaded or is not enabled. "
                 ."Some distributions split this extension off into a "
                 ."separate package, usually with a name like 'php5-".$Ext."'.";
        }
    }

    # check to make sure directories are writable
    $Cwd = getcwd();
    global $G_DirsThatMustBeWritable;
    foreach ($G_DirsThatMustBeWritable as $Dir)
    {
        # the directory must have a forward slash in the beginning since the
        # directory from getcwd() will not have a trailing slash
        $Dir = ($Dir{0} != "/") ? "/".$Dir : $Dir;

        $Dir = $Cwd.$Dir;
        if (!is_dir($Dir))
        {
            @mkdir($Dir, 0777);
            if (!is_dir($Dir))
            {
                $ErrMsgs[] = "Directory <i>".$Dir."</i> could not be created.";
            }
        }

        if (is_dir($Dir) && !is_writable($Dir))
        {
            @chmod($Dir, 0777);
            if (!is_writable($Dir))
            {
                $ErrMsgs[] = "Directory <i>".$Dir."</i> is not writable.";
            }
        }
    }

    # return updated error message list to caller
    return $ErrMsgs;
}

function NormalizeInstallInfo($FVars)
{
    # return normalized info to caller
    return $FVars;
}

function CheckInstallInfo($FVars, $ErrMsgs)
{
    # check MySQL availability and version and that we can create tables in DB
    if (!strlen(trim($FVars["F_DBHost"]))
            || !strlen(trim($FVars["F_DBLogin"]))
            || !strlen(trim($FVars["F_DBPassword"])))
    {
        if (!strlen(trim($FVars["F_DBHost"])))
            {  $ErrMsgs["F_DBHost"] = "No database host was supplied.";  }
        if (!strlen(trim($FVars["F_DBLogin"])))
            {  $ErrMsgs["F_DBLogin"] = "No database login was supplied.";  }
        if (!strlen(trim($FVars["F_DBPassword"])))
            {  $ErrMsgs["F_DBPassword"] = "No database password was supplied.";  }
    }
    else
    {
        $DBHandle = @mysqli_connect(
                $FVars["F_DBHost"], $FVars["F_DBLogin"], $FVars["F_DBPassword"]);
        if ($DBHandle === FALSE)
            {  $ErrMsgs[] = "Could not connect to database on ".$FVars["F_DBHost"].".";  }
        else
        {
            global $G_MinimumMysqlVersion;
            global $G_CwisVersion;
            if (version_compare(mysqli_get_server_info($DBHandle), $G_MinimumMysqlVersion) == -1)
            {
                $ErrMsgs[] = "Required MySQL version not found."
                        ." CWIS ".$G_CwisVersion." requires MySQL version "
                                .$G_MinimumMysqlVersion." or later."
                        ."<br />MySQL version <i>".mysqli_get_server_info($DBHandle)
                                ."</i> was detected.";
            }
            else
            {
                if (!strlen(trim($FVars["F_DBName"])))
                    {  $ErrMsgs["F_DBName"] = "No database name was supplied.";  }
                elseif (!mysqli_select_db($DBHandle, $FVars["F_DBName"]))
                {
                    if (mysqli_query($DBHandle, "CREATE DATABASE `"
                            .addslashes($FVars["F_DBName"])."`") === FALSE)
                        {  $ErrMsgs["F_DBName"] = "Could not create database.";  }
                    else
                    {
                        mysqli_query($DBHandle, "DROP DATABASE `".addslashes($FVars["F_DBName"])."`");
                    }
                }
            }
            mysqli_close($DBHandle);
        }
    }

    # check to make sure that admin info looks valid (if not upgrading)
    if ($FVars["F_Submit"] != "Upgrade Installation")
    {
        global $G_MinimumPasswordLength;
        if (!strlen(trim($FVars["F_AdminLogin"])))
        {
            $ErrMsgs["F_AdminLogin"] = "No administrative account login was supplied.";
        }
        if (!strlen(trim($FVars["F_AdminPassword"])))
        {
            $ErrMsgs["F_AdminPassword"] = "No administrative account password was supplied.";
        }
        if (strlen(trim($FVars["F_AdminPassword"]))<$G_MinimumPasswordLength)
        {
            $ErrMsgs["F_AdminPassword"] = "Administrative password supplied was too short."
                ." Password must be at least ".$G_MinimumPasswordLength." characters long.";
        }
        $UnholyEmailAddressRegx = "/^[-_a-z0-9\'+*$^&%=~!?{}]++(?:\."
                ."[-_a-z0-9\'+*$^&%=~!?{}]+)*+@(?:(?![-.])[-a-z0-9.]+"
                ."(?<![-.])\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d++)?$/iD";
        if (!strlen(trim($FVars["F_AdminEmail"])))
        {
            $ErrMsgs["F_AdminEmail"] =
                    "No administrative account e-mail address was supplied.";
        }
        elseif (!preg_match($UnholyEmailAddressRegx, trim($FVars["F_AdminEmail"])))
        {
            $ErrMsgs["F_AdminEmail"] =
                    "An invalid administrative account e-mail address was supplied.";
        }
        elseif (trim($FVars["F_AdminEmailAgain"]) != trim($FVars["F_AdminEmail"]))
        {
            $ErrMsgs["F_AdminEmailAgain"] =
                    "The two administrative account e-mail addresses did not match.";
        }
    }

    # return updated error message list to caller
    return $ErrMsgs;
}

function CheckDistributionFiles($ErrMsgs, $NewVersion)
{
    # error out if checksum file not found or not readable
    if (!file_exists("install/CHECKSUMS"))
    {
        $ErrMsgs[] = "Checksum file <i>install/CHECKSUMS</i> not found.";
        return $ErrMsgs;
    }
    elseif (!is_readable("install/CHECKSUMS"))
    {
        $ErrMsgs[] = "Checksum file <i>install/CHECKSUMS</i> not readable.";
        return $ErrMsgs;
    }

    # load in MD5 checksums
    $Lines = file("install/CHECKSUMS", FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
    $Checksums = array();
    foreach ($Lines as $Line)
    {
        $Pieces = preg_split("/\s+/", $Line, 2);
        $Checksums[$Pieces[1]] = $Pieces[0];
    }

    # for each checksum
    $ErrorCount = 0;
    foreach ($Checksums as $FileName => $Chksum)
    {
        # if file exists
        if (file_exists($FileName))
        {
            # generate checksum for file
            $CalcChksum = md5_file($FileName);

            # if checksums do not match
            if (strtolower($CalcChksum) != strtolower($Chksum))
            {
                # record error message about checksum error
                $ErrMsgs[] = "Checksum does not match for CWIS ".$NewVersion
                        ." distribution file <i>".$FileName."</i>.";
                $ErrorCount++;
            }
        }
        else
        {
            # record error message about missing file
            $ErrMsgs[] = "Distribution file <i>".$FileName."</i> not found.";
            $ErrorCount++;
        }

        # quit if too many errors encountered
        if ($ErrorCount > 20)
        {
            $ErrMsgs[] = "More than 20 errors encountered when checking"
                    ." distribution file integrity.  If files were uploaded"
                    ." to the web server via FTP, please make sure they"
                    ." were transferred using \"Binary\" mode, rather than"
                    ." \"ASCII\" or \"Automatic\" mode.";
            break;
        }
    }

    # return updated error message list to caller
    return $ErrMsgs;
}



# ----- INSTALLATION ---------------------------------------------------------

function InstallFiles($FVars, $IsUpgrade)
{
    # set up config file with DB info if not present
    $ErrMsgs = array();
    if (!file_exists("local/config.php") && !file_exists("config.php"))
    {
        Msg(1, "Creating configuration file...");
        $ConfigReplacements = array(
                "X-DBUSER-X" => addslashes($FVars["F_DBLogin"]),
                "X-DBPASSWORD-X" => addslashes($FVars["F_DBPassword"]),
                "X-DBHOST-X" => addslashes($FVars["F_DBHost"]),
                "X-DBNAME-X" => addslashes($FVars["F_DBName"]),
                );
        $ErrMsg = CopyFile(
                "install/config.php.DIST", "local/config.php",
                $ConfigReplacements);
        if ($ErrMsg) {  $ErrMsgs[] = $ErrMsg;  }
    }

    # if there is no .htaccess file
    if (!file_exists(".htaccess"))
    {
        # set up .htaccess file
        Msg(1, "Creating .htaccess file...");
        $ErrMsg = InstallHtaccess($FVars, ".htaccess");
        if ($ErrMsg) { $ErrMsgs []= $ErrMsg; }
    }
    else
    {
        # if .htaccess file appears to be from a previous CWIS release
        $OldChecksums = GetFileChecksums("install/htaccess.CHECKSUMS");

        $Lines = file(".htaccess");
        $Content = "";
        foreach ($Lines as $Line)
        {
            if (!preg_match("/^RewriteBase/", $Line))
            {
                $Content .= $Line;
            }
        }
        $Checksum = md5($Content);
        Msg(3, "Checksum for current .htaccess file:  ".$Checksum);
        if (in_array($Checksum, $OldChecksums))
        {
            # replace .htaccess file
            Msg(1, "Replacing .htaccess file...");
            $ErrMsg = InstallHtaccess($FVars, ".htaccess");
            if ($ErrMsg) { $ErrMsgs []= $ErrMsg; }
        }
        else
        {
            # create .htaccess template file
            Msg(1, "Creating .htaccess template file...");
            $ErrMsg = InstallHtaccess($FVars, ".htaccess.CWIS");
            if ($ErrMsg) { $ErrMsgs []= $ErrMsg; }
        }
    }

    # check if robots.txt exists
    if (!file_exists("robots.txt"))
    {
        # if not, create it
        Msg(1, "Creating robots.txt file...");
        $ErrMsg = CopyFile("install/robots.txt.DIST", "robots.txt");
        if ($ErrMsg) { $ErrMsgs []= $ErrMsg; }
    }
    else
    {
        # if so, see if it came from an older CWIS version
        $OldChecksums = GetFileChecksums("install/robots.txt.CHECKSUMS");
        $Checksum = md5(file_get_contents("robots.txt"));

        Msg(3, "Checksum for current robots.txt file:  ".$Checksum);
        if (in_array($Checksum, $OldChecksums))
        {
            # it was from an older version, replace it
            Msg(1, "Replacing robots.txt file...");
            $ErrMsg = CopyFile("install/robots.txt.DIST", "robots.txt");
            if ($ErrMsg) { $ErrMsgs []= $ErrMsg; }
        }
        else
        {
            # otherwise create robots.txt template file
            Msg(1, "Creating .htaccess template file...");
            $ErrMsg = CopyFile("install/robots.txt.DIST", "robots.txt.CWIS");
            if ($ErrMsg) { $ErrMsgs []= $ErrMsg; }
        }
    }

    # return any error messages to caller
    return $ErrMsgs;
}

function InstallHtaccess($FVars, $FileName)
{
    # set up .htaccess file with correct rewrite base
    $Pieces = parse_url($FVars["F_SiteUrl"]);
    $BasePath = trim($Pieces["path"]);
    $BasePath = preg_match("%/$%", $BasePath) ? $BasePath : dirname($BasePath);
    if ($BasePath == "//") {  $BasePath = "/";  }
    $ConfigReplacements = array(
            "X-REWRITEBASE-X" => $BasePath,
            );
    $ErrMsg = CopyFile(
            "install/htaccess.DIST", $FileName,
            $ConfigReplacements);

    # return any error messages to caller
    return $ErrMsg;
}

function SetUpNewDatabase($FVars, $ErrMsgs)
{
    # create database if necessary
    $DBHandle = mysqli_connect(
            $FVars["F_DBHost"], $FVars["F_DBLogin"], $FVars["F_DBPassword"]);
    $Result = mysqli_select_db($DBHandle, $FVars["F_DBName"]);
    if ($Result === FALSE)
    {
        $Result = mysqli_query($DBHandle, "CREATE DATABASE `".$FVars["F_DBName"]."`");
        if ($Result === FALSE)
        {
            $ErrMsgs[] = "Could not create database <i>".$FVars["F_DBName"]."</i>.";
            return $ErrMsgs;
        }
        $Result = mysqli_select_db($DBHandle, $FVars["F_DBName"]);
    }

    # set default storage engine (need MyISAM for full-text indexing)
    if (version_compare(mysqli_get_server_info($DBHandle), "5.5", "<"))
    {
        mysqli_query($DBHandle, "SET storage_engine=MYISAM");
    }
    else
    {
        mysqli_query($DBHandle,"SET default_storage_engine=MYISAM");
    }

    # set up database tables
    global $G_SqlErrorsWeCanIgnore;
    global $G_DatabaseSetupFiles;
    foreach ($G_DatabaseSetupFiles as $SqlFile => $Msg)
    {
        Msg(1, $Msg);
        $ErrMsg = ExecuteSqlFile($DBHandle, $SqlFile, $G_SqlErrorsWeCanIgnore, 4);
        if ($ErrMsg)
        {
            $ErrMsgs[] = $ErrMsg;
            return $ErrMsgs;
        }
    }

    # return (possibly updated) error message list to caller
    mysqli_close($DBHandle);
    return $ErrMsgs;
}

function LoadDefaultConfiguration($FVars, $ErrMsgs)
{
    # load default configuration
    Msg(1, "Loading default configuration...");

    # load the default configuration
    require_once 'install/LoadDefaultConfig.php';

    # check for errors
    $ErrMsg = isset($GLOBALS["G_ErrMsg"]) ? $GLOBALS["G_ErrMsg"] : NULL;

    # an error occurred
    if ($ErrMsg)
    {
        $ErrMsgs[] = $ErrMsg;
    }

    # everything went fine
    else
    {
        # reload the system configuration and set the admin e-mail
        $GLOBALS["G_SysConfig"] = new SystemConfiguration();
        $GLOBALS["G_SysConfig"]->AdminEmail($FVars["F_AdminEmail"]);
    }

    # return (possibly updated) error message list to caller
    return $ErrMsgs;
}

function InitializeAF()
{
    # initialize application environment
    Msg(1, "Initializing application framework...");
    require_once("include/StartUp.php");
}

function SetUpNewSite($FVars, $ErrMsgs)
{
    # create schemas
    if (MetadataSchema::SchemaExistsWithId(MetadataSchema::SCHEMAID_DEFAULT))
    {
        $ResourceSchema = new MetadataSchema(MetadataSchema::SCHEMAID_DEFAULT);
    }
    else
    {
        $ResourceSchema = MetadataSchema::Create("Resources");
    }
    $ResourceSchema->ViewPage("index.php?P=FullRecord&ID=\$ID");
    if (MetadataSchema::SchemaExistsWithId(MetadataSchema::SCHEMAID_USER))
    {
        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
    }
    else
    {
        $UserSchema = MetadataSchema::Create("Users");
    }
    $UserSchema->ViewPage("index.php?P=UserList");

    # load qualifiers
    Msg(1, "Loading qualifiers...");
    $ErrMsg = ImportQualifiersFromXml("install/Qualifiers.xml");
    if ($ErrMsg !== NULL)
    {
        $ErrMsgs[] = $ErrMsg;
        return $ErrMsgs;
    }

    # load metadata fields
    $SchemaFiles = array(
            "DCMI" => "Dublin Core",
            "Administrative" => "administrative",
            );
    foreach ($SchemaFiles as $SchemaSuffix => $SchemaDescription)
    {
        Msg(1, "Loading ".$SchemaDescription." fields for default resource schema...");
        $Result = $ResourceSchema->AddFieldsFromXmlFile(
                "install/MetadataSchema--".$SchemaSuffix.".xml");
        if ($Result === FALSE)
        {
            $SchemaErrors = $ResourceSchema->ErrorMessages();
            foreach ($SchemaErrors as $Errors)
            {
                $ErrMsgs = array_merge($ErrMsgs, $Errors);
            }
            return $ErrMsgs;
        }
    }

    # load user metadata fields
    Msg(1, "Loading fields for user resource schema...");
    $Result = $UserSchema->AddFieldsFromXmlFile(
            "install/MetadataSchema--User.xml");
    if ($Result === FALSE)
    {
        $SchemaErrors = $UserSchema->ErrorMessages();
        foreach ($SchemaErrors as $Errors)
        {
            $ErrMsgs = array_merge($ErrMsgs, $Errors);
        }
        return $ErrMsgs;
    }

    # set up default authoring privileges for resource schema
    Msg(1, "Setting privileges for default resource schema...");
    $APrivs = new PrivilegeSet();
    $APrivs->AddPrivilege(PRIV_RESOURCEADMIN);
    $APrivs->AddPrivilege(PRIV_MYRESOURCEADMIN);
    $ResourceSchema->AuthoringPrivileges($APrivs);

    # set up default editing privileges for resource schema
    $EPrivs = new PrivilegeSet();
    $EPrivs->AddPrivilege(PRIV_RESOURCEADMIN);
    $Subgroup = new PrivilegeSet();
    $Subgroup->AllRequired(TRUE);
    $Subgroup->AddPrivilege(PRIV_MYRESOURCEADMIN);
    $FieldId = $ResourceSchema->GetFieldIdByName("Added By Id");
    $Subgroup->AddCondition($FieldId);
    $EPrivs->AddSet($Subgroup);
    $ResourceSchema->EditingPrivileges($EPrivs);

    # set up default viewing privileges for resource schema
    $VPrivs = new PrivilegeSet();
    if (($FieldId = $ResourceSchema->GetFieldIdByName("Release Flag")) !== FALSE)
    {
        $VPrivs->AddCondition($FieldId, 1);
    }
    $VPrivs->AddCondition(PrivilegeSet::HAVE_RESOURCE, FALSE, "==");
    $VPrivs->AddPrivilege(PRIV_RESOURCEADMIN);
    $Subgroup = new PrivilegeSet();
    $Subgroup->AllRequired(TRUE);
    $Subgroup->AddPrivilege(PRIV_MYRESOURCEADMIN);
    $FieldId = $ResourceSchema->GetFieldIdByName("Added By Id");
    $Subgroup->AddCondition($FieldId);
    $VPrivs->AddSet($Subgroup);
    $ResourceSchema->ViewingPrivileges($VPrivs);

    # setup the default privileges for user schema
    Msg(1, "Setting privileges for user resource schema...");

    # get the newly-created user ID field
    $UserIdField = $UserSchema->GetFieldByName("UserId");

    # setup the default privileges and set them
    $ViewingPrivileges = new PrivilegeSet();
    $AuthoringPrivileges = new PrivilegeSet();
    $EditingPrivileges = new PrivilegeSet();
    $ViewingPrivileges->AddCondition($UserIdField);
    $ViewingPrivileges->AddPrivilege(PRIV_USERADMIN);
    $ViewingPrivileges->AddPrivilege(PRIV_SYSADMIN);
    $EditingPrivileges->AddCondition($UserIdField);
    $EditingPrivileges->AddPrivilege(PRIV_USERADMIN);
    $EditingPrivileges->AddPrivilege(PRIV_SYSADMIN);
    $UserSchema->ViewingPrivileges($ViewingPrivileges);
    $UserSchema->AuthoringPrivileges($AuthoringPrivileges);
    $UserSchema->EditingPrivileges($EditingPrivileges);

    # create administrative account
    $UFactory = new CWUserFactory();
    if (!$UFactory->UserNameExists($FVars["F_AdminLogin"]))
    {
        Msg(1, "Adding administrator account...");
        $Admin = $UFactory->CreateNewUser(
                $FVars["F_AdminLogin"],
                $FVars["F_AdminPassword"],
                $FVars["F_AdminPassword"],
                $FVars["F_AdminEmail"],
                $FVars["F_AdminEmail"]);
        if (is_object($Admin))
        {
            $Admin->IsActivated(TRUE);
            $Admin->GrantPriv(PRIV_SYSADMIN);
            $Admin->GrantPriv(PRIV_NEWSADMIN);
            $Admin->GrantPriv(PRIV_RESOURCEADMIN);
            $Admin->GrantPriv(PRIV_FORUMADMIN);
            $Admin->GrantPriv(PRIV_CLASSADMIN);
            $Admin->GrantPriv(PRIV_NAMEADMIN);
            $Admin->GrantPriv(PRIV_RELEASEADMIN);
	    $Admin->GrantPriv(PRIV_USERADMIN);
	    $Admin->GrantPriv(PRIV_POSTTOFORUMS);
	    $Admin->GrantPriv(PRIV_POSTCOMMENTS);
	    $Admin->GrantPriv(PRIV_MYRESOURCEADMIN);
            $Admin->GrantPriv(PRIV_COLLECTIONADMIN);
        }
        else
        {
            foreach ($Admin as $ErrCode)
            {
                $ErrMsgs[] = "Error creating administrator account ("
                        .$ErrCode.").";
            }
            return $ErrMsgs;
        }
    }

    # set admin as G_User for Resource::Create()
    $GLOBALS["G_User"] = $Admin;

    # load sample records
    Msg(1, "Loading sample records...");
    $ErrMsg = ImportResourcesFromXml("install/SampleResources.xml");
    if ($ErrMsg !== NULL)
    {
        $ErrMsgs[] = $ErrMsg;
        return $ErrMsgs;
    }

    # clean up our temporary global settings
    unset($GLOBALS["G_User"]);

    # return unchanged error message list to caller
    return $ErrMsgs;
}

function UpgradeExistingDatabase($FVars, $ErrMsgs, $OldVersion)
{
    # if legacy configuration file was used
    if (!file_exists("local/config.php"))
    {
        # write out configuration file in new location
        Msg(1, "Migrating configuration file...");
        $ConfigReplacements = array(
                "X-DBUSER-X" => addslashes($FVars["F_DBLogin"]),
                "X-DBPASSWORD-X" => addslashes($FVars["F_DBPassword"]),
                "X-DBHOST-X" => addslashes($FVars["F_DBHost"]),
                "X-DBNAME-X" => addslashes($FVars["F_DBName"]),
                );
        $ErrMsg = CopyFile(
                "install/config.php.DIST", "local/config.php",
                $ConfigReplacements);
        if ($ErrMsg) {  $ErrMsgs[] = $ErrMsg;  }

        # rename legacy configuration file so it won't be loaded
        rename("config.php", "OLD.config.php");
    }

    # open connection to database
    $DBHandle = mysqli_connect(
            $FVars["F_DBHost"], $FVars["F_DBLogin"], $FVars["F_DBPassword"]);
    $Result = mysqli_select_db($DBHandle, $FVars["F_DBName"]);
    if ($Result === FALSE)
    {
        $ErrMsgs[] = "Could not connect to database <i>"
                .$FVars["F_DBName"]."</i> to upgrade.";
        return $ErrMsgs;
    }

    # for each available database upgrade file
    $SqlFileNames = ReadDirectory("install/DBUpgrades/.", "/DBUpgrade--.*\.sql/");
    $PhpFileNames = ReadDirectory("install/DBUpgrades/.", "/DBUpgrade--.*\.php/");
    $FileNames = array_merge($SqlFileNames, $PhpFileNames);
    # (use custom sort function so that SQL files execute before PHP files)
    function CompareDBUpgradeFileNames($A, $B)
    {
        if ($A == $B) {  return 0;  }
        list($ABase1, $ABase2, $ABase3, $AType) = explode(".", $A);
        $ABase = $ABase1.".".$ABase2.".".$ABase3;
        list($BBase1, $BBase2, $BBase3, $BType) = explode(".", $B);
        $BBase = $BBase1.".".$BBase2.".".$BBase3;
        if ($ABase == $BBase)
        {
            return ($AType < $BType) ? 1 : -1;
        }
        else
        {
            return ($ABase < $BBase) ? -1 : 1;
        }
    }
    usort($FileNames, "CompareDBUpgradeFileNames");
    foreach ($FileNames as $FileName)
    {
        # parse out version number of upgrade file
        $UpgradeVersion = str_replace("DBUpgrade--", "", $FileName);
        $UpgradeVersion = str_replace(".sql", "", $UpgradeVersion);
        $UpgradeVersion = str_replace(".php", "", $UpgradeVersion);

        # if upgrade file version is greater than old version
        if (strcmp($UpgradeVersion, $OldVersion) > 0)
        {
            # add file to list of those to be run
            $FilesToRun["install/DBUpgrades/".$FileName] =
                    "Upgrading database to version ".$UpgradeVersion
                    .(preg_match("/.php/", $FileName) ? " (PHP)" : "")
                    ."...";
        }
    }

    # if there were upgrades to be done
    if (isset($FilesToRun))
    {
        # add entry to test database permissions at start of upgrade
        $Test = array("install/TestDBPerms.sql" => "Testing database permissions...");
        $FilesToRun = $Test + $FilesToRun;

        # for each file
        foreach ($FilesToRun as $FileName => $Msg)
        {
            # if file was PHP upgrade file
            Msg(1, $Msg);
            if (preg_match("/.php/", $FileName))
            {
                # run PHP for upgrade
                include($FileName);
                $ErrMsg = isset($GLOBALS["G_ErrMsg"]) ? $GLOBALS["G_ErrMsg"] : NULL;
                unset($GLOBALS["G_ErrMsg"]);
            }
            # else file was SQL upgrade file
            else
            {
                # run SQL for upgrade
                $ErrMsg = ExecuteSqlFile($DBHandle, $FileName,
                        $GLOBALS["G_SqlErrorsWeCanIgnore"], 4);
            }

            # if errors were encountered
            if ($ErrMsg)
            {
                # add error messages to list
                $ErrMsgs[] = $ErrMsg;

                # stop running upgrades
                break;
            }
        }
    }

    # return any error messages to caller
    return $ErrMsgs;
}

/**
* Perform version-specific site upgrades (after application framework has
* been intialized), from PHP files stored in install/SiteUpgrades.  Any code
* run must be idempotent, and any error messages should be returned by
* putting them in an array in $GLOBALS["G_ErrMsgs"].  Upgrade files should
* be named "SiteUpgrade--VERSION.php", where VERSION is the version being
* upgraded to.
* @param string $OldVersion Version we are upgrading from.
* @return array Error messages (or empty array if no errors).
*/
function UpgradeSite($OldVersion)
{
    # for each available database upgrade file
    $FileNames = ReadDirectory("install/SiteUpgrades/.", "/SiteUpgrade--.*\.php/");
    foreach ($FileNames as $FileName)
    {
        # parse out version number of upgrade file
        $UpgradeVersion = str_replace("SiteUpgrade--", "", $FileName);
        $UpgradeVersion = str_replace(".php", "", $UpgradeVersion);

        # if upgrade file version is greater than old version
        if (strcmp($UpgradeVersion, $OldVersion) > 0)
        {
            # add file to list of those to be run
            $FilesToRun["install/SiteUpgrades/".$FileName] =
                    "Upgrading site to version ".$UpgradeVersion."...";
        }
    }

    # if there were upgrades to be done
    if (isset($FilesToRun))
    {
        # for each file
        foreach ($FilesToRun as $FileName => $Msg)
        {
            # run PHP for upgrade
            Msg(1, $Msg);
            include($FileName);

            # if error was encountered
            if (isset($GLOBALS["G_ErrMsgs"]))
            {
                # add error to error message list
                $ErrMsgs = $GLOBALS["G_ErrMsgs"];
                unset($GLOBALS["G_ErrMsgs"]);

                # stop running upgrades
                break;
            }
        }
    }

    # return error messages (if any) to caller
    return isset($ErrMsgs) ? $ErrMsgs : array();
}

# (returns NULL if not upgrading)
function CheckForUpgrade()
{
    # if both old and new version files are present
    $OldVersion = NULL;
    if (file_exists("VERSION") && file_exists("NEWVERSION"))
    {
        # read in old version
        $InputFile = fopen("VERSION", "r");
        $OldVersion = trim(fgets($InputFile, 256));
        fclose($InputFile);

        # read in new version
        $InputFile = fopen("NEWVERSION", "r");
        $NewVersion = trim(fgets($InputFile, 256));
        fclose($InputFile);

        # if new version is the same as old version
        if ($NewVersion == $OldVersion)
        {
            # clear old version
            $OldVersion = NULL;
        }
        # else if new version is older than old version
        elseif ($NewVersion < $OldVersion)
        {
            # ???
        }
    }

    # return old version number to caller
    return $OldVersion;
}

function LoadOldInstallInfo(&$FVars, $ErrMsgs)
{
    # load values from existing configuration file
    if (file_exists("local/config.php"))
    {
        include("local/config.php");
    }
    elseif (file_exists("config.php"))
    {
        include("config.php");
    }
    elseif (file_exists("include/SPT--Config.php"))
    {
        include("include/SPT--Config.php");
    }

    $Protocol = isset($_SERVER["HTTPS"]) ? "https://" : "http://";

    if (array_key_exists("G_Config", $GLOBALS) && is_array($GLOBALS["G_Config"]))
    {
        $FVars["F_DBHost"] = $GLOBALS["G_Config"]["Database"]["Host"];
        $FVars["F_DBLogin"] = $GLOBALS["G_Config"]["Database"]["UserName"];
        $FVars["F_DBPassword"] = $GLOBALS["G_Config"]["Database"]["Password"];
        $FVars["F_DBName"] = $GLOBALS["G_Config"]["Database"]["DatabaseName"];
        $FVars["F_DefaultUI"] = $GLOBALS["G_Config"]["UserInterface"]["DefaultUI"];
    }
    else
    {
        $FVars["F_DBHost"] = $SPT_DBHost;
        $FVars["F_DBLogin"] = $SPT_DBUserName;
        $FVars["F_DBPassword"] = $SPT_DBPassword;
        $FVars["F_DBName"] = $SPT_DBName;
        $FVars["F_DefaultUI"] = $SPT_DefaultUI;
    }
    $FVars["F_Submit"] = "Upgrade Installation";
    $FVars["F_SiteUrl"] = $Protocol.$_SERVER["HTTP_HOST"]
            .dirname($_SERVER["SCRIPT_NAME"])."/";

    # return any error messages to caller
    return $ErrMsgs;
}

function QueueFollowUpWork($FVars, $IsUpgrade, $OldVersion)
{
    if ($IsUpgrade)
    {
        $Callback = "PerformUpgradeFollowUp";
        $Parameters = array($OldVersion);
        $Description = "Upgrade Follow-Up";
    }
    else
    {
        $Callback = "PerformNewInstallFollowUp";
        $Parameters = NULL;
        $Description = "New Installation Follow-Up";
    }
    $GLOBALS["AF"]->QueueUniqueTask($Callback, $Parameters,
            ApplicationFramework::PRIORITY_HIGH, $Description);
}


# ----- UTILITY FUNCTIONS ----------------------------------------------------

/**
* Copy a file, optionally doing keyword replacement on the contents.
* @param string SrcFile Name of original file.
* @param string DstFile Name of new file.
* @param array|NULL Replacements with keys giving keywords to search
*     for and values giving resplacements.
* @return NULL if successful or error message if failed.
*/
function CopyFile($SrcFile, $DstFile, $Replacements=NULL)
{
    # read source file contents
    $Text = @file_get_contents($SrcFile);
    if ($Text === FALSE) {  return "Unable to open file <i>".$SrcFile."</i>.";  }

    if ($Replacements !== NULL)
    {
        # make substitutions
        $Text = str_replace(array_keys($Replacements), $Replacements, $Text);
    }

    # write out destination file
    $Result = @file_put_contents($DstFile, $Text);
    if ($Result === FALSE) {  return "Unable to write file <i>".$DstFile."</i>.";  }

    # return NULL to caller to indicate success
    return NULL;
}

function ExecuteSqlFile($DBHandle, $FileName, $ErrorsToIgnore, $DebugLevel)
{
    # open input file
    $FHandle = fopen($FileName, "r");
    if ($FHandle === FALSE)
    {
        return "Unable to open SQL file <i>".$FileName."</i>";
    }

    # while lines left in DB setup file and everything is going okay
    $CurrentCommand = "";
    $CurrentDisplay = array();
    while (!feof($FHandle))
    {
        # read in line from DB setup file
        $Line = fgets($FHandle, 8192);

        # trim whitespace from line
        $TLine = trim($Line);

        # if line is not a comment
        if ((preg_match("/^#/", $TLine) == FALSE)
                && (preg_match("/^--/", $TLine) == FALSE)
                && strlen($TLine))
        {
            # add line to current command
            $CurrentCommand .= " ".$TLine;
            $CurrentDisplay[] = rtrim($Line);

            # if line ends command
            if (preg_match("/;$/", $TLine) == TRUE)
            {
                # execute command
                foreach ($CurrentDisplay as $DLine)
                    {  Msg($DebugLevel, preg_replace("/^ /", "&nbsp;&nbsp;", $DLine));  }
                $CurrentCommand = preg_replace("/;$/", "", $CurrentCommand);
                $Result = mysqli_query($DBHandle, $CurrentCommand);

                # if command failed
                if ($Result === FALSE)
                {
                    # if error was one we should not ignore
                    $IgnoreError = FALSE;
                    foreach ($ErrorsToIgnore as $SqlPattern => $ErrMsgPattern)
                    {
                        $SqlPattern = preg_replace("/\\s+/", "\\s+", $SqlPattern);
                        if (preg_match($SqlPattern, $CurrentCommand)
                                && preg_match($ErrMsgPattern, mysqli_error($DBHandle)))
                        {
                            $IgnoreError = TRUE;
                            break;
                        }
                    }
                    if ($IgnoreError === FALSE)
                    {
                        # bail out with error message
                        $ErrMsg = "Database command failed:\n";
                        foreach ($CurrentDisplay as $DLine)
                            {  $ErrMsg .= "<pre>".$DLine."</pre>\n";  }
                        $ErrMsg .= "(SQL error: <i>".mysqli_error($DBHandle)."</i>)";
                        return $ErrMsg;
                    }
                }

                # clear current command
                $CurrentCommand = "";
                $CurrentDisplay = array();
            }
        }
    }

    # close input file
    fclose($FHandle);

    # report back to caller that everything worked
    return NULL;
}

function ReadDirectory($Path, $PerlExp = "")
{
    # return empty list if path points to directory that does not exist
    if (!is_dir($Path)) {  return array();  }

    # while file names left to read from directory
    $FileNames = array();
    $Dir = opendir($Path);
    while ($FileName = readdir($Dir))
    {
        # if name matches mask
        if (($PerlExp != "") && preg_match($PerlExp, $FileName))
        {
            # store file name in array
            $FileNames[count($FileNames)] = $FileName;
        }
    }
    closedir($Dir);

    # return sorted array of file names to caller
    sort($FileNames);
    return $FileNames;
}

/**
* Read in checksums for older versions of distribution files.
* @param string $SrcFile containing VERSION\tMD5 for old versions.  Lines starting
*   with a hashmark are ignored.
* @return array(VERSION => MD5)
*/
function GetFileChecksums($SrcFile)
{
    $OldChecksums = array();

    $Lines = file($SrcFile);
    foreach ($Lines as $Line)
    {
        if (!preg_match("/^#/", $Line))
        {
            list($Version, $Sum) = explode(" ", $Line);
            $OldChecksums[$Version] = trim($Sum);
        }
    }

    return $OldChecksums;
}

function Msg($VerbLvl, $Message)
{
    global $G_VerbosityLevel;
    if ($VerbLvl <= $G_VerbosityLevel)
    {
        for ($Index = $VerbLvl;  $Index > 1;  $Index--) {  print("&nbsp;&nbsp;");  }
        print($Message."<br />\n");
        LogMsg($Message);
    }
}

function LogMsg($Message)
{
    static $FHandle = FALSE;
    if ($FHandle == FALSE)
    {
        $InstallLogFile = "local/logs/install.log";
        if (!is_dir(dirname($InstallLogFile))
                && is_writable(dirname(dirname($InstallLogFile))))
        {
            mkdir(dirname(dirname($InstallLogFile)));
        }
        if ((file_exists($InstallLogFile) && is_writable($InstallLogFile))
                || (!file_exists($InstallLogFile)
                        && is_writable(dirname($InstallLogFile))))
        {
            $FHandle = fopen($InstallLogFile, "a");
        }
    }
    if ($FHandle)
    {
        $LogMsg = date("Y-m-d H:i:s")."  ".strip_tags($Message)."\n";
        fwrite($FHandle, $LogMsg);
        fflush($FHandle);
    }
}

/**
* Import qualifier entries from XML file.
* @param string $FileName Name of XML file.
* @return string Error message or NULL if execution was successful.
*/
function ImportQualifiersFromXml($FileName)
{
    # open file
    $In = new XMLReader();
    $In->open($FileName);

    # while XML left to read
    while ($In->read())
    {
        # if new element
        if ($In->nodeType == XMLReader::ELEMENT)
        {
            # if node indicates start of entry
            if ($In->name === "Qualifier")
            {
                # create a new qualifier
                $Qualifier = new Qualifier();
            }
            else
            {
                # if we have a current qualifier
                if (isset($Qualifier))
                {
                    # retrieve tag and value
                    $Tag = $In->name;
                    $In->read();
                    $Value = $In->value;
                    $In->read();

                    # set attribute of qualifier based on tag
                    switch ($Tag)
                    {
                        case "Name":
                            $Qualifier->Name($Value);
                            break;

                        case "Namespace":
                            $Qualifier->NSpace($Value);
                            break;

                        case "Url":
                            $Qualifier->Url($Value);
                            break;

                        default:
                            break;
                    }
                }
            }
        }
    }

    # close file
    $In->close();

    # report success to caller
    return NULL;
}

/**
* Import resource records from XML file.
* @param string $FileName Name of XML file.
* @param int $SchemaId ID of metadata schema to use for import.
* @return string Error message or NULL if execution was successful.
*/
function ImportResourcesFromXml($FileName,
        $SchemaId = MetadataSchema::SCHEMAID_DEFAULT)
{
    # open file
    $In = new XMLReader();
    $In->open($FileName);

    # load possible tag names
    $PossibleTags = array();
    $Schema = new MetadataSchema($SchemaId);
    $Fields = $Schema->GetFields();
    foreach ($Fields as $FieldId => $Field)
    {
        $PossibleTags[$Field->DBFieldName()] = $Field;
    }

    # while XML left to read
    while ($In->read())
    {
        # if new element
        if ($In->nodeType == XMLReader::ELEMENT)
        {
            # if node indicates start of resource
            if ($In->name === "Resource")
            {
                # if we already had a resource make it non-temporary
                if (isset($Resource)) {  $Resource->IsTempResource(FALSE);  }

                # create a new resource
                $Resource = Resource::Create($SchemaId);
            }
            # else if node is in list of possible tags
            elseif (array_key_exists($In->name, $PossibleTags))
            {
                # if we have a current resource
                if (isset($Resource))
                {
                    # retrieve field and value
                    $DBFieldName = $In->name;
                    $Field = $PossibleTags[$DBFieldName];
                    $In->read();
                    $Value = $In->value;
                    $In->read();

                    # set value in resource based on field type
                    switch ($Field->Type())
                    {
                        case MetadataSchema::MDFTYPE_TEXT:
                        case MetadataSchema::MDFTYPE_PARAGRAPH:
                        case MetadataSchema::MDFTYPE_NUMBER:
                        case MetadataSchema::MDFTYPE_TIMESTAMP:
                        case MetadataSchema::MDFTYPE_URL:
                        case MetadataSchema::MDFTYPE_DATE:
                            $Resource->Set($Field, $Value);
                            break;

                        case MetadataSchema::MDFTYPE_FLAG:
                            $Resource->Set($Field,
                                    (strtoupper($Value) == "TRUE") ? TRUE : FALSE);
                            break;

                        case MetadataSchema::MDFTYPE_OPTION:
                        case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                            if (!isset($CNFact))
                                    {  $CNFact = new ControlledNameFactory();  }
                            $CName = $CNFact->GetItemByName($Value);
                            if ($CName === NULL)
                            {
                                $CNFact->ClearCaches();
                                $CName = new ControlledName(NULL, $Value, $Field->Id());
                            }
                            $Resource->Set($Field, $CName);
                            break;

                        case MetadataSchema::MDFTYPE_TREE:
                            if (!isset($CFact))
                                    {  $CFact = new ClassificationFactory();  }
                            $Class = $CFact->GetItemByName($Value);
                            if ($Class === NULL)
                            {
                                $CFact->ClearCaches();
                                $Class = Classification::Create($Value, $Field->Id());
                            }
                            $Resource->Set($Field, $Class);
                            break;

                        case MetadataSchema::MDFTYPE_POINT:
                            list($Point["X"], $Point["Y"]) = explode(",", $Value);
                            $Resource->Set($Field, $Point);
                            break;

                        case MetadataSchema::MDFTYPE_USER:
                            if (preg_match("/^[0-9]+\$/", $Value))
                            {
                                $Value = intval($Value);
                            }
                            $Resource->Set($Field, $Value);
                            break;

                        case MetadataSchema::MDFTYPE_IMAGE:
                        case MetadataSchema::MDFTYPE_FILE:
                        case MetadataSchema::MDFTYPE_REFERENCE:
                            break;

                        default:
                            break;
                    }
                }
            }
        }
    }

    # make final resource (if any) non-temporary
    if (isset($Resource)) {  $Resource->IsTempResource(FALSE);  }

    # close file
    $In->close();

    # report success to caller
    return NULL;
}


# ----- MAIN -----------------------------------------------------------------

# have all output write out immediately
ob_implicit_flush(TRUE);

# initialize starting values
$ErrMsgs = array();
$FVars = $_POST;

# set debug output level
if (isset($_POST["F_Debug"])) {  $G_VerbosityLevel = 2;  }
if (isset($_POST["F_MoreDebug"])) {  $G_VerbosityLevel = 3;  }
if (isset($_POST["F_EvenMoreDebug"])) {  $G_VerbosityLevel = 4;  }
if (isset($_GET["VB"])) {  $G_VerbosityLevel = $_GET["VB"];  }

# grab our version number
$NewVersion = file_exists("NEWVERSION") ? rtrim(file_get_contents("NEWVERSION")) : "";

# begin page
BeginHtmlPage($NewVersion);

# check environment to make sure we can run
$ErrMsgs = CheckEnvironment($ErrMsgs);

# check distribution files
if (!array_key_exists("NOCHKSUMS", $_GET))
{
    $ErrMsgs = CheckDistributionFiles($ErrMsgs, $NewVersion);
}

# if problems were found with environment
if (count($ErrMsgs))
{
    # display error messages
    PrintErrorMessages($ErrMsgs);
}
else
{
    # if we are upgrading
    $OldVersion = CheckForUpgrade();
    $IsUpgrade = $OldVersion ? TRUE : FALSE;
    if ($IsUpgrade)
    {
        # if existing version is too old to upgrade
        if ($OldVersion < $G_OldestVersionWeCanUpgrade)
        {
            $ErrMsgs[] = "Your currently installed version (<i>".$OldVersion
                    ."</i>) is too old to be upgraded with this package. "
                    ."You must first upgrade to version ".$G_OldestVersionWeCanUpgrade
                    ." and then use this package.";
        }
        else
        {
            # load install information from existing files
            $ErrMsgs = LoadOldInstallInfo($FVars, $ErrMsgs);
        }
    }

    # if we have install information
    if (isset($FVars["F_Submit"]) && !count($ErrMsgs))
    {
        # normalize install info
        $FVars = NormalizeInstallInfo($FVars);

        # check installation information
        $ErrMsgs = CheckInstallInfo($FVars, $ErrMsgs);
    }

    # if install information has not been collected or we encountered errors
    if (!isset($FVars["F_Submit"]) || count($ErrMsgs))
    {
        # display any error messages
        PrintErrorMessages($ErrMsgs);

        # if this is an upgrade
        if ($IsUpgrade)
        {
            # display pointer to helpful info
            PrintHelpPointers();
        }
        else
        {
            # display install information form
            PrintInstallInfoForm($FVars, $ErrMsgs);
        }
    }
    else
    {
        # print install parameter summary
        PrintInstallInfoSummary($FVars, $IsUpgrade);

        # set up files
        Msg(1, "<b>Beginning ".($IsUpgrade ? "Upgrade" : "Installation")
                ." Process...</b>");
        $ErrMsgs = InstallFiles($FVars, $IsUpgrade);

        # if we are upgrading
        if ($IsUpgrade)
        {
            # upgrade existing database
            if (!count($ErrMsgs))
            {
                $ErrMsgs = UpgradeExistingDatabase($FVars, $ErrMsgs, $OldVersion);
            }

            # initialize application environment
            if (!count($ErrMsgs)) {  InitializeAF();  }

            # upgrade site
            if (!count($ErrMsgs))
                    {  $ErrMsgs = UpgradeSite($OldVersion);  }
        }
        else
        {
            # set up database
            if (!count($ErrMsgs))
                    {  $ErrMsgs = SetUpNewDatabase($FVars, $ErrMsgs);  }

            # initialize application environment
            if (!count($ErrMsgs)) {  InitializeAF();  }

            # set up site
            if (!count($ErrMsgs))
                    {  $ErrMsgs = SetUpNewSite($FVars, $ErrMsgs);  }

            # load default system configuration
            if (!count($ErrMsgs))
                    {  $ErrMsgs = LoadDefaultConfiguration($FVars, $ErrMsgs);  }
        }

        # if errors encountered
        if (count($ErrMsgs))
        {
            # display any error messages
            PrintErrorMessages($ErrMsgs);

            # display pointer to helpful info
            PrintHelpPointers();
        }
        else
        {
            # queue new install or upgrade follow-up work as appropriate
            QueueFollowUpWork($FVars, $IsUpgrade, $OldVersion);

            # declare victory
            Msg(1, "<b>".($IsUpgrade ? "Upgrade" : "Installation")
                    ." Process Completed</b>");

            # print install complete message
            PrintInstallCompleteInfo($FVars, $IsUpgrade);

            # set version
            if ($IsUpgrade)
            {
                if (file_exists("OLDVERSION")) {  unlink("OLDVERSION");  }
                rename("VERSION", "OLDVERSION");
            }
            rename("NEWVERSION", "VERSION");

            # clean up after ourselves
            if (file_exists("installcwis.php.SAVE")) {  unlink("installcwis.php.SAVE");  }
            rename("installcwis.php", "installcwis.php.SAVE");
        }
    }
}

# end page
EndHtmlPage();
