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(); }
/** * 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."); }
/** * 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."); }