protected function init() { $this->setDescription('Create a new TestSwarm project. Returns the auth token (can be re-created with refreshProjectToken.php).'); $this->registerOption('create', 'boolean', 'Pass this to the create if it doesn\'t exist.'); $this->registerOption('id', 'value', 'ID of project (must be in format: "' . LoginAction::getNameValidationRegex() . '").'); $this->registerOption('display-title', 'value', 'Display title (free form text, max: 255 chars)'); $this->registerOption('password', 'value', 'Password for this project (omit to enter in interactive mode)'); $this->registerOption('site-url', 'value', 'URL for this project (optional)'); }
protected function init() { $this->setDescription('Create or update a TestSwarm project.'); $this->registerOption('create', 'boolean', 'Pass this to the create if it doesn\'t exist.'); $this->registerOption('delete', 'boolean', 'Pass this to remove a project and recursively delete all its jobs and runs.'); $this->registerOption('id', 'value', 'ID of project (must be in format: "' . LoginAction::getNameValidationRegex() . '").'); $this->registerOption('display-title', 'value', 'Display title (free form text, max: 255 chars)'); $this->registerOption('password', 'value', 'Password for this project (omit to enter in interactive mode)'); $this->registerOption('site-url', 'value', 'URL for this project (optional)'); }
/** * 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."); }
/** * @param string $id * @param array $options * @return array Exposes the new auth token */ public function create($id, array $options = null) { $db = $this->getContext()->getDB(); $password = isset($options['password']) ? $options['password'] : null; $displayTitle = isset($options['displayTitle']) ? $options['displayTitle'] : null; $siteUrl = isset($options['siteUrl']) ? $options['siteUrl'] : ''; if (!$id || !$displayTitle || !$password) { $this->setError('missing-parameters'); return; } // Check if a project by this id doesn't exist already $row = $db->getOne(str_queryf('SELECT id FROM projects WHERE id = %s;', $id)); if ($row) { $this->setError('invalid-input', 'Unable to create project, a project by that name exists already.'); return; } // Validate project id if (!LoginAction::isValidName($id)) { $this->setError('invalid-input', 'Project ids must be in format: "' . LoginAction::getNameValidationRegex() . '".'); return; } // maxlength (otherwise MySQL will crop it) if (strlen($displayTitle) > 255) { $this->setError('Display title has to be no longer than 255 characters.'); return; } // Create the project $authToken = LoginAction::generateRandomHash(40); $authTokenHash = sha1($authToken); $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);', $id, $displayTitle, $siteUrl, LoginAction::generatePasswordHash($password), $authTokenHash, swarmdb_dateformat(SWARM_NOW), swarmdb_dateformat(SWARM_NOW))); if (!$isInserted) { $this->setError('internal-error', 'Insertion of row into database failed.'); return; } return array('authToken' => $authToken); }