Beispiel #1
0
 public function execute()
 {
     $action = SignupAction::newFromContext($this->getContext());
     $action->doAction();
     $error = $action->getError();
     if (!$error) {
         $data = $action->getData();
         if ($data["status"] === "logged-in") {
             $this->redirect(swarmpath("user/" . $data["username"]));
         }
     }
     $this->setAction($action);
     $this->content = $this->initContent();
 }
Beispiel #2
0
    /**
     * The actual database updates
     * Friendly reminder from http://dev.mysql.com/doc/refman/5.1/en/alter-table.html
     * - Column name must be mentioned twice in ALTER TABLE CHANGE
     * - Definition must be complete
     *   So 'CHANGE foo BIGINT' on a 'foo INT UNSIGNED DEFAULT 1' will remove
     *   the default and unsigned property.
     *   Except for PRIMARY KEY or UNIQUE properties, those must never be
     *   part of a CHANGE clause.
     */
    protected function doDatabaseUpdates()
    {
        if ($this->getContext()->dbLock()) {
            $this->error('Database is currently locked, please remove ./cache/database.lock before updating.');
        }
        $db = $this->getContext()->getDB();
        $this->out('Setting database.lock, other requests may not access the database during the update.');
        $this->getContext()->dbLock(true);
        $this->out('Running tests on the database to detect which updates are needed.');
        /**
         * 0.2.0 -> 1.0.0-alpha (patch-new-ua-runresults.sql)
         * useragents and run_client table removed, many column changes, new runresults table.
         */
        // If the previous version was before 1.0.0 we won't offer an update, because most
        // changes in 1.0.0 can't be simulated without human intervention. The changes are not
        // backwards compatible. Instead do a few quick checks to verify this is in fact a
        // pre-1.0.0 database, then ask the user for a re-install from scratch
        // (except for the users table).
        $has_run_client = $db->tableExists('run_client');
        $has_users_request = $db->fieldExists('users', 'request');
        $clients_useragent_id = $db->fieldInfo('clients', 'useragent_id');
        if (!$clients_useragent_id) {
            $this->unknownDatabaseState('clients.useragent_id not found');
            return;
        }
        if (!$has_run_client && !$has_users_request && !$clients_useragent_id->numeric && $clients_useragent_id->type === 'string') {
            $this->out('... run_client table already dropped');
            $this->out('... users.request already dropped');
            $this->out('... client.useragent_id is up to date');
        } else {
            $this->out("\n" . "It appears this database is from before 1.0.0. No update exists for those versions.\n" . "The updater could re-install TestSwarm (optionally preserving user accounts)\n" . "THIS WILL DELETE ALL DATA.\nContinue? (Y/N)");
            $reinstall = $this->cliInput();
            if ($reinstall !== 'Y') {
                // Nothing left to do. Remove database.lock and abort the script
                $this->getContext()->dbLock(false);
                return;
            }
            $this->out("Import user names and tokens from the old database after re-installing?\n" . "(Note: password and seed cannot be restored due to incompatibility in the database.\n" . ' Instead the auth token will be used as the the new password) (Y/N)');
            $reimportUsers = $this->cliInput();
            // Drop all known TestSwarm tables in the database
            // (except users, handled separately)
            foreach (array('run_client', 'clients', 'run_useragent', 'useragents', 'runs', 'jobs') as $dropTable) {
                $this->outRaw("Dropping {$dropTable} table...");
                $exists = $db->tableExists($dropTable);
                if ($exists) {
                    $dropped = $db->query('DROP TABLE ' . $db->addIdentifierQuotes($dropTable));
                    $this->out(' ' . ($dropped ? 'OK' : 'FAILED'));
                } else {
                    $this->out('SKIPPED (didn\'t exist)');
                }
            }
            // Handle users table (reimport or drop as well)
            $userRows = array();
            if ($reimportUsers === 'Y') {
                $this->out('Upgrading users table');
                $this->outRaw('Fetching current users...');
                $has_users = $db->tableExists('users');
                if (!$has_users) {
                    $this->out('SKIPPED (users table didn\'t exist)');
                } else {
                    $userRows = $db->getRows('SELECT * FROM users');
                    $this->out('OK');
                }
            }
            $this->outRaw('Dropping users table...');
            $dropped = $db->query('DROP TABLE users');
            $this->out(' ' . ($dropped ? 'OK' : 'FAILED'));
            // Create new tables
            $this->outRaw('Creating new tables... (this may take a few minutes)');
            global $swarmInstallDir;
            $fullSchemaFile = "{$swarmInstallDir}/config/tables.sql";
            if (!is_readable($fullSchemaFile)) {
                $this->error('Can\'t read schema file');
            }
            $fullSchemaSql = file_get_contents($fullSchemaFile);
            $executed = $db->batchQueryFromFile($fullSchemaSql);
            if (!$executed) {
                $this->error('Creating new tables failed');
            }
            $this->out('OK');
            if ($reimportUsers === 'Y') {
                $this->out('Re-importing ' . count($userRows) . ' users...');
                foreach ($userRows as $userRow) {
                    $this->outRaw('- creating user "' . $userRow->name . '"... ');
                    if (empty($userRow->password) || empty($userRow->seed) || empty($userRow->auth)) {
                        $this->out('SKIPPED: Not a project account but a swarm client.');
                        continue;
                    }
                    try {
                        $signupAction = SignupAction::newFromContext($this->getContext());
                        // Password stored in the old database is a hash of the old seed (of type 'double'
                        // and the actual password. We can't create this user with the same password because
                        // sha1 is not supposed to be decodable.
                        // I tried overriding the created row after the creation with the old seed and password,
                        // but that didn't work because the old seed doesn't fit in the new seed field (of binary(40)).
                        // When inserted, mysql transforms it into something else and sha1(seed + password) will no
                        // longer match the hash. So instead create the new user with the auth token as password.
                        $signupAction->doCreateUser($userRow->name, $userRow->auth);
                        $err = $signupAction->getError();
                        if (!$err) {
                            $this->outRaw('OK. Restoring auth token... ');
                            $data = $signupAction->getData();
                            $updated = $db->query(str_queryf('UPDATE users
								SET
									auth = %s
								WHERE id = %u', $userRow->auth, $data['userID']));
                            $this->out($updated ? 'OK.' : 'FAILED.');
                        } else {
                            $this->out("FAILED. SignupAction error. {$err['info']}");
                        }
                    } catch (Exception $e) {
                        $this->out("FAILED. Unexpected exception thrown while creating account. {$e->getMessage()}");
                    }
                }
            }
            // End of users re-import
        }
        // End of patch-new-ua-runresults.sql
        /**
         * 1.0.0-alpha (patch-users-projects-conversion.sql)
         * users table removed, new projects table, various column changes.
         */
        $has_users = $db->tableExists('users');
        $has_clients_user_id = $db->fieldInfo('clients', 'user_id');
        $has_jobs_user_id = $db->fieldInfo('jobs', 'user_id');
        $has_projects = $db->tableExists('projects');
        $has_clients_name = $db->fieldInfo('clients', 'name');
        $has_jobs_project_id = $db->fieldInfo('jobs', 'project_id');
        $has_clients_useragent_id = $db->fieldInfo('clients', 'useragent_id');
        if (!$has_users && !$has_clients_user_id && !$has_jobs_user_id) {
            $this->out('... users table already dropped');
            $this->out('... clients.user_id already dropped');
            $this->out('... jobs.user_id already dropped');
        } else {
            // Verify that the entire database is in the 1.0.0-alpha2012 state,
            // not just part of it.
            foreach (array('users table' => $has_users, 'clients.user_id' => $has_clients_user_id, 'jobs.user_id' => $has_jobs_user_id, 'projects table' => !$has_projects, 'clients.name' => !$has_clients_name, 'jobs.project_id' => !$has_jobs_project_id, 'clients.useragent_id' => $has_clients_useragent_id) as $label => $isAsExpected) {
                if (!$isAsExpected) {
                    $this->unknownDatabaseState($label . ' not found');
                    return;
                }
            }
            $this->out('Schema changes before users-projects-conversion migration...');
            $this->out('... creating projects table');
            $db->query("CREATE TABLE `projects` (\n  `id` varchar(255) binary NOT NULL PRIMARY KEY,\n  `display_title` varchar(255) binary NOT NULL,\n  `site_url` blob NOT NULL default '',\n  `password` tinyblob NOT NULL,\n  `auth_token` tinyblob NOT NULL,\n  `updated` binary(14) NOT NULL,\n  `created` binary(14) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
            $this->out('... adding clients.name');
            $db->query("ALTER TABLE clients\n  ADD `name` varchar(255) binary NOT NULL AFTER `id`");
            $this->out('... adding jobs.project_id');
            $db->query("ALTER TABLE jobs\n  ADD `project_id` varchar(255) binary NOT NULL AFTER `name`");
            $this->out('... dropping constraint fk_clients_user_id');
            $db->query("ALTER TABLE clients\n  DROP FOREIGN KEY fk_clients_user_id");
            $this->out('... dropping constraint fk_jobs_user_id');
            $db->query("ALTER TABLE jobs\n  DROP FOREIGN KEY fk_jobs_user_id");
            $this->out('... dropping constraint fk_runs_job_id');
            $db->query("ALTER TABLE runs\n  DROP FOREIGN KEY fk_runs_job_id");
            $this->out('... dropping constraint fk_run_useragent_run_id');
            $db->query("ALTER TABLE run_useragent\n  DROP FOREIGN KEY fk_run_useragent_run_id");
            $this->out('... dropping constraint fk_runresults_client_id');
            $db->query("ALTER TABLE runresults\n  DROP FOREIGN KEY fk_runresults_client_id");
            $this->out('... dropping index idx_users_name');
            $db->query("ALTER TABLE users\n  DROP INDEX idx_users_name");
            $this->out('... dropping index idx_clients_user_useragent_updated');
            $db->query("ALTER TABLE clients\n  DROP INDEX idx_clients_user_useragent_updated");
            $this->out('... dropping index idx_jobs_user');
            $db->query("ALTER TABLE jobs\n  DROP INDEX idx_jobs_user");
            $this->out('Migrating old content into new schema...');
            $this->out('... fetching users table');
            $userRows = $db->getRows('SELECT * FROM users') ?: array();
            $this->out('... found ' . count($userRows) . ' users');
            foreach ($userRows as $userRow) {
                $this->out('... creating project "' . $userRow->name . '"');
                if (!trim($userRow->seed) || !trim($userRow->password) || !trim($userRow->auth)) {
                    // Client.php used to create rows in the users table with blanks
                    // in these "required" fields. MySQL expands the emptyness to the full
                    // 40-width of the column. Hence the trim().
                    $this->out('    SKIPPED: Not a project account but a swarm client.');
                    continue;
                }
                // Validate project id
                if (!LoginAction::isValidName($userRow->name)) {
                    $this->out('    SKIPPED: User name not a valid project id. Must match: ' . LoginAction::getNameValidationRegex());
                    continue;
                }
                if (!$db->getOne(str_queryf('SELECT 1 FROM jobs WHERE user_id=%u', $userRow->id))) {
                    $this->out('    SKIPPED: Account has 0 jobs');
                    continue;
                }
                $isInserted = $db->query(str_queryf('INSERT INTO projects
					(id, display_title, site_url, password, auth_token, updated, created)
					VALUES(%s, %s, %s, %s, %s, %s, %s);', $userRow->name, $userRow->name, '', LoginAction::generatePasswordHashForUserrow($userRow), sha1($userRow->auth), swarmdb_dateformat(SWARM_NOW), $userRow->created));
                if (!$isInserted) {
                    $this->out('    FAILED: Failed to insert row into projects table.');
                    continue;
                }
                $this->out('... updating references for project "' . $userRow->name . '"');
                $isUpdated = $db->query(str_queryf('UPDATE clients
					SET name=%s
					WHERE user_id=%u', $userRow->name, $userRow->id));
                if (!$isUpdated) {
                    $this->out('    FAILED: Failed to update rows in clients table.');
                    continue;
                }
                $isUpdated = $db->query(str_queryf('UPDATE jobs
					SET project_id=%s
					WHERE user_id=%u', $userRow->name, $userRow->id));
                if (!$isUpdated) {
                    $this->out('    FAILED: Failed to update rows in jobs table.');
                    continue;
                }
            }
            $this->out('Schema changes after users-projects-conversion migration...');
            $this->out('... changing clients.useragent_id');
            $db->query("ALTER TABLE clients\n  CHANGE COLUMN `useragent_id` `useragent_id` varchar(255) NOT NULL");
            $this->out('... dropping clients.user_id');
            $db->query("ALTER TABLE clients\n  DROP COLUMN `user_id`");
            $this->out('... dropping jobs.user_id');
            $db->query("ALTER TABLE jobs\n  DROP COLUMN `user_id`");
            $this->out('... dropping users table');
            $db->query("DROP TABLE users");
            $this->out('... adding index idx_clients_name_ua_created');
            $db->query("ALTER TABLE clients\n  ADD INDEX idx_clients_name_ua_created (name, useragent_id, created);");
            $this->out('... adding index idx_jobs_project_created');
            $db->query("ALTER TABLE jobs\n  ADD INDEX idx_jobs_project_created (project_id, created);");
        }
        // End of patch-users-projects-conversion.sql
        $this->getContext()->dbLock(false);
        $this->out("Removed database.lock.\nNo more updates.");
    }
Beispiel #3
0
 /**
  * The actual database updates
  *  Friendly reminder from http://dev.mysql.com/doc/refman/5.1/en/alter-table.html
  * - Column name must be mentioned twice in ALTER TABLE CHANGE
  * - Definition must be complete
  *   So 'CHANGE foo BIGINT' on a 'foo INT UNSIGNED DEFAULT 1' will remove
  *   the default and unsigned property.
  *   Except for PRIMARY KEY or UNIQUE properties, those must never be
  *   part of a CHANGE clause.
  */
 protected function doDatabaseUpdates()
 {
     if ($this->getContext()->dbLock()) {
         $this->error("Database is currently locked, please remove ./cache/database.lock before updating.");
     }
     $db = $this->getContext()->getDB();
     $this->out("Setting database.lock, other requests may not access the database during the update.");
     $this->getContext()->dbLock(true);
     $this->out("Executing tests on the database to detect where updates are needed...");
     // 1.0.0: useragents table removed, and many column changes.
     // If the previous version was before 1.0.0 we won't bother changing a ton of stuff
     // because most changes in 1.0.0 aren't easily convertable. Instead do a few quick
     // checks to verify this is in fact a pre 1.0.0 database, ask the user for
     // confirmation, and instead re-install from scratch, re-importing only the users.
     $has_useragents = $db->tableExists('useragents');
     $has_users_request = $db->fieldExists('users', 'request');
     $clients_useragent_id = $db->fieldInfo('clients', 'useragent_id');
     if (!is_object($clients_useragent_id)) {
         $this->unknownDatabaseState('clients.useragent_id not found');
         return;
     }
     if (!$has_useragents && !$has_users_request && !$clients_useragent_id->numeric && $clients_useragent_id->type === "string") {
         $this->out("...useragents already dropped");
         $this->out("...users.request already dropped");
         $this->out("...client.useragent_id is up to date");
     } else {
         $this->out("\n" . "It appears this database is from before 1.0.0. No upgrade path exists for those versions.\n" . "The updater could re-install TestSwarm (optionally importing old users)\n" . "THIS WILL DELETE ALL DATA.\nContinue? (Y/N)");
         $reinstall = $this->cliInput();
         if ($reinstall !== "Y") {
             // Nothing left to do. Remove database.lock and abort the script
             $this->getContext()->dbLock(false);
             return;
         }
         $this->out("Import user names and tokens from the old database after re-installing?\n" . "(Note: password and seed cannot be restored due to incompatibility in the database.\n" . " Instead the auth token will be used as the the new password) (Y/N)");
         $reimportUsers = $this->cliInput();
         // Drop all known TestSwarm tables in the database
         // (except users, handled separately)
         foreach (array('run_client', 'clients', 'run_useragent', 'useragents', 'runs', 'jobs') as $dropTable) {
             $this->outRaw("Dropping {$dropTable} table...");
             $exists = $db->tableExists($dropTable);
             if ($exists) {
                 $dropped = $db->query("DROP TABLE " . $db->addIdentifierQuotes($dropTable));
                 $this->out(" " . ($dropped ? "OK" : "FAILED"));
             } else {
                 $this->out("SKIPPED (didn't exist)");
             }
         }
         // Handle users table (reimport or drop as well)
         $userRows = array();
         if ($reimportUsers === "Y") {
             $this->out("Upgrading users table");
             $this->outRaw("Fetching current users...");
             $has_users = $db->tableExists('users');
             if (!$has_users) {
                 $this->out("SKIPPED (users table didn't exist)");
             } else {
                 $userRows = $db->getRows("SELECT * FROM users");
                 $this->out("OK");
             }
         }
         $this->outRaw("Dropping users table...");
         $dropped = $db->query("DROP TABLE users");
         $this->out(" " . ($dropped ? "OK" : "FAILED"));
         // Create new tables
         $this->outRaw("Creating new tables... (this may take a few minutes)");
         global $swarmInstallDir;
         $fullSchemaFile = "{$swarmInstallDir}/config/testswarm.sql";
         if (!is_readable($fullSchemaFile)) {
             $this->error("Can't read testswarm.sql");
         }
         $fullSchemaSql = file_get_contents($fullSchemaFile);
         $executed = $db->batchQueryFromFile($fullSchemaSql);
         if (!$executed) {
             $this->error("Creating new tables failed");
         }
         $this->out("OK");
         if ($reimportUsers === "Y") {
             $this->out("Re-importing " . count($userRows) . " users...");
             foreach ($userRows as $userRow) {
                 $this->outRaw("- creating user '" . $userRow->name . "'... ");
                 if (empty($userRow->password) || empty($userRow->seed) || empty($userRow->auth)) {
                     $this->out("SKIPPED: Not a real account but a swarm client.");
                     continue;
                 }
                 try {
                     $signupAction = SignupAction::newFromContext($this->getContext());
                     // Password stored in the old datbase is a hash of the old seed (of type 'double'_
                     // and the actual password. We can't create this user with the same password because
                     // sha1 is not supposed to be decodable.
                     // I tried overriding the created row after the creation with the old seed and password,
                     // but that didn't work because the old seed doesn't fit in the new seed field (of binary(40)).
                     // When inserted mysql transforms it into something else and sha1(seed + password) will no
                     // longer match the hash. So instead create the new user with the auth token as password.
                     $signupAction->doCreateUser($userRow->name, $userRow->auth);
                     $err = $signupAction->getError();
                     if (!$err) {
                         $this->outRaw("OK. Restoring auth token... ");
                         $data = $signupAction->getData();
                         $updated = $db->query(str_queryf("UPDATE users\n\t\t\t\t\t\t\t\tSET\n\t\t\t\t\t\t\t\t\tauth = %s\n\t\t\t\t\t\t\t\tWHERE id = %u", $userRow->auth, $data["userID"]));
                         $this->out($updated ? "OK." : "FAILED.");
                     } else {
                         $this->out("FAILED. SignupAction error. {$err["info"]}");
                     }
                 } catch (Exception $e) {
                     $this->out("FAILED. Unexpected exception thrown while creating account. {$e->getMessage()}");
                 }
             }
         }
         // End of users re-import
     }
     // End of 1.0.0-alpha update
     $this->getContext()->dbLock(false);
     $this->out("Removed database.lock.\nNo more updates.");
 }