@@ -22,9 +22,10 @@ public function execute() {
}

protected function initContent() {
$conf = $this->getContext()->getConf();
$request = $this->getContext()->getRequest();
$browserInfo = $this->getContext()->getBrowserInfo();
$context = $this->getContext();
$conf = $context->getConf();
$request = $context->getRequest();
$browserInfo = $context->getBrowserInfo();

$siteNameHtml = htmlspecialchars( $conf->web->title );

@@ -44,21 +45,16 @@ protected function initContent() {
$html .= '<div class="span5"><div class="well">';
if ( !$conf->client->requireRunToken ) {
if ( $browserInfo->isInSwarmUaIndex() ) {
$html .= '<p><strong>Join ' . $siteNameHtml . '!</strong><br>'
. ' You have a browser that we need to test against, join the swarm to help us out!</p>';
if ( !$request->getSessionData( 'username' ) ) {
$html .= '<form action="' . swarmpath( '' ) . '" method="get" class="form-horizontal">'
. '<input type="hidden" name="action" value="run">'
. '<div class="input-append">'
. '<label for="form-item">Username:</label>'
. '<input type="text" name="item" id="form-item" placeholder="Enter username..">'
. '<input type="submit" value="Join the swarm" class="btn btn-primary">'
. '</div>'
. '</form>';
} else {
$html .= '<p><a href="' . swarmpath( "run/{$request->getSessionData( 'username' )}" )
. '" class="btn btn-primary btn-large">Join the swarm</a></p>';
}
$auth = $context->getAuth();
$suggestedClientName = $auth ? $auth->project->id : '';
$html .= '<p>Your browser is in our index, run some tests!</p>'
. '<form action="' . swarmpath( '' ) . '" method="get" class="form-horizontal swarm-form-join">'
. '<input type="hidden" name="action" value="run">'
. '<div class="input-append">'
. '<input type="text" name="item" placeholder="Enter name.." value="' . htmlspecialchars( $suggestedClientName ) . '" required pattern="' . htmlspecialchars( Client::getNameValidationRegex() ) . '">'
. '<input type="submit" value="Join the swarm" class="btn btn-primary">'
. '</div>'
. '</form>';
} else {
$uaData = $browserInfo->getUaData();
unset( $uaData->displayInfo );
@@ -69,7 +65,7 @@ protected function initContent() {
. ' <a href="https://github.com/jquery/testswarm/issues">Issue Tracker</a>,'
. ' including the following 2 codes:'
. '<br><strong><a href="https://github.com/tobie/ua-parser">ua-parser</a>:</strong> <code>'
. htmlspecialchars( print_r( $uaData, true ) )
. htmlspecialchars( print_r( $uaData, true ) )
. '</code><br><strong><a href="https://en.wikipedia.org/wiki/User_agent" title="Read about User agent on Wikipedia!">User-Agent</a> string:</strong> <code>'
. htmlspecialchars( $browserInfo->getRawUA() )
. '</code></p>';
@@ -20,6 +20,7 @@ public function execute() {

protected function initContent() {
$request = $this->getContext()->getRequest();
$auth = $this->getContext()->getAuth();

$this->setTitle( "Job status" );
$this->setRobots( "noindex,nofollow" );
@@ -33,23 +34,23 @@ protected function initContent() {
$html .= html_tag( 'div', array( 'class' => 'alert alert-error' ), $error['info'] );
}

if ( !isset( $data["jobInfo"] ) ) {
if ( !isset( $data["info"] ) ) {
return $html;
}

$this->setSubTitle( '#' . $data["jobInfo"]["id"] );
$this->setSubTitle( '#' . $data["info"]["id"] );

$isAuth = $request->getSessionData( "auth" ) === "yes" && $data["jobInfo"]["ownerName"] == $request->getSessionData( "username" );
$isOwner = $auth && $auth->project->id === $data["info"]["projectID"];

$html .=
'<h2>' . $data["jobInfo"]["name"] .'</h2>'
'<h2>' . $data["info"]["nameHtml"] .'</h2>'
. '<p><em>Submitted by '
. html_tag( "a", array( "href" => swarmpath( "user/{$data["jobInfo"]["ownerName"]}" ) ), $data["jobInfo"]["ownerName"] )
. ' on ' . htmlspecialchars( date( "Y-m-d H:i:s", gmstrtotime( $data["jobInfo"]["creationTimestamp"] ) ) )
. ' (UTC)' . '</em>.</p>';
. html_tag( "a", array( "href" => swarmpath( "project/{$data["info"]["projectID"]}" ) ), $data["info"]["projectID"] )
. ' '. self::getPrettyDateHtml( $data["info"], 'created' )
. '</em>.</p>';

if ( $isAuth ) {
$html .= '<script>SWARM.jobInfo = ' . json_encode( $data["jobInfo"] ) . ';</script>';
if ( $isOwner ) {
$html .= '<script>SWARM.jobInfo = ' . json_encode( $data["info"] ) . ';</script>';
$action_bar = '<div class="form-actions swarm-item-actions">'
. ' <button class="swarm-reset-runs-failed btn btn-info">Reset failed runs</button>'
. ' <button class="swarm-reset-runs btn btn-info">Reset all runs</button>'
@@ -64,14 +65,17 @@ protected function initContent() {
$html .= '<table class="table table-bordered swarm-results"><thead>'
. self::getUaHtmlHeader( $data['userAgents'] )
. '</thead><tbody>'
. self::getUaRunsHtmlRows( $data['runs'], $data['userAgents'], $isAuth )
. self::getUaRunsHtmlRows( $data['runs'], $data['userAgents'], $isOwner )
. '</tbody></table>';

$html .= $action_bar;

return $html;
}

/**
* Create a table header for user agents.
*/
public static function getUaHtmlHeader( $userAgents ) {
$html = '<tr><th>&nbsp;</th>';
foreach ( $userAgents as $userAgent ) {
@@ -93,8 +97,11 @@ public static function getUaHtmlHeader( $userAgents ) {
}

/**
* @param Array $runs
* @param Array $userAgents
* Create table rows for a table of ua run results.
* This is used on the JobPage.
*
* @param Array $runs List of runs, from JobAction.
* @param Array $userAgents List of uaData objects.
* @param bool $showResetRun: Whether to show the reset buttons for individual runs.
* This does not check authororisation or load related javascript for the buttons.
*/
@@ -130,7 +137,7 @@ public static function getUaRunsHtmlRows( $runs, $userAgents, $showResetRun = fa
$runResultsTagOpen
. ( $uaRun['runResultsLabel']
? $uaRun['runResultsLabel']
: UserPage::getStatusIconHtml( $uaRun['runStatus'] )
: self::getStatusIconHtml( $uaRun['runStatus'] )
). '</a>'
. $runResultsTagOpen
. html_tag( 'i', array(
@@ -146,7 +153,7 @@ public static function getUaRunsHtmlRows( $runs, $userAgents, $showResetRun = fa
: ''
);
} else {
$html .= UserPage::getStatusIconHtml( $uaRun['runStatus'] );
$html .= self::getStatusIconHtml( $uaRun['runStatus'] );
}
$html .= '</td>';
} else {
@@ -158,4 +165,94 @@ public static function getUaRunsHtmlRows( $runs, $userAgents, $showResetRun = fa

return $html;
}

public static function getStatusIconHtml( $status ) {
static $icons = array(
"new" => '<i class="icon-time" title="Scheduled, awaiting run."></i>',
"progress" => '<i class="icon-repeat swarm-status-progressicon" title="In progress.."></i>',
"passed" => '<i class="icon-ok" title="Passed!"></i>',
"failed" => '<i class="icon-remove" title="Completed with failures"></i>',
"timedout" => '<i class="icon-flag" title="Maximum execution time exceeded"></i>',
"error" => '<i class="icon-warning-sign" title="Aborted by an error"></i>',
);
return isset( $icons[$status] ) ? $icons[$status] : '';
}

/**
* Not used anywhere yet. The colors, icons and tooltips should be
* easy to understand. If not, this table is ready for use.
* @example:
* '<div class="row"><div class="span6">' . getStatusLegend() . '</div></div>'
*/
public static function getStatusLegend() {
return
'<table class="table table-condensed table-bordered swarm-results">'
. '<tbody>'
. '<tr><td class="swarm-status swarm-status-new">'
. self::getStatusIconHtml( "new" )
. '</td><td>Scheduled</td>'
. '</tr>'
. '<tr><td class="swarm-status swarm-status-progress">'
. self::getStatusIconHtml( "progress" )
. '</td><td>In progress..</td>'
. '</tr>'
. '<tr><td class="swarm-status swarm-status-passed">'
. self::getStatusIconHtml( "passed" )
. '</td><td>Passed!</td>'
. '</tr>'
. '<tr><td class="swarm-status swarm-status-failed">'
. self::getStatusIconHtml( "failed" )
. '</td><td>Completed with failures</td>'
. '</tr>'
. '<tr><td class="swarm-status swarm-status-timedout">'
. self::getStatusIconHtml( "timedout" )
. '</td><td>Maximum execution time exceeded</td>'
. '</tr>'
. '<tr><td class="swarm-status swarm-status-error">'
. self::getStatusIconHtml( "error" )
. '</td><td>Aborted by an error</td>'
. '</tr>'
. '<tr><td class="swarm-status swarm-status-notscheduled">'
. ''
. '</td><td>This browser was not part of the browserset for this job.</td>'
. '</tr>'
. '</tbody></table>';
}

/**
* Create a single row summarising the ua runs of a job. See also #getUaRunsHtmlRows.
* This is used on the ProjectPage.
* @param Array $job
* @param Array $userAgents List of uaData objects.
*/
public static function getJobHtmlRow( $job, $userAgents ) {
$html = '<tr><th>'
. '<a href="' . htmlspecialchars( $job['info']['viewUrl'] ) . '">' . htmlspecialchars( $job['info']['nameText'] ) . '</a>'
. ' ' . self::getPrettyDateHtml( $job['info'], 'created', array( 'class' => 'swarm-result-date' ) )
. "</th>\n";

foreach ( $userAgents as $uaID => $uaData ) {
$html .= self::getJobStatusHtmlCell( isset( $job['summaries'][$uaID] ) ? $job['summaries'][$uaID] : false );
}

$html .= '</tr>';
return $html;

}

/**
* Create a singe cell summarising the ua runs of a job. See also #getJobHtmlRow.
* This is used on the ProjectsPage.
* @param string|bool $status Status, or false to create a "skip" cell with
* "notscheduled" status.
*/
public static function getJobStatusHtmlCell( $status = false ) {
return $status
? ( '<td class="swarm-status-cell"><div class="swarm-status swarm-status-' . $status . '">'
. self::getStatusIconHtml( $status )
. '</div></td>'
)
: '<td class="swarm-status swarm-status-notscheduled"></td>';
}

}
@@ -3,7 +3,7 @@
* "Login" page.
*
* @author John Resig, 2008-2011
* @author Timo Tijhof, 2012
* @author Timo Tijhof, 2012-2013
* @since 0.1.0
* @package TestSwarm
*/
@@ -16,8 +16,8 @@ public function execute() {
$error = $action->getError();
if ( !$error ) {
$data = $action->getData();
if ( $data["status"] === "logged-in" ) {
$this->redirect( swarmpath( "user/" . $data["username"] ) );
if ( $data ) {
$this->redirect( swarmpath( 'project/' . $data['id'] ) );
}
}

@@ -42,18 +42,16 @@ protected function initContent() {

$html .=
'<div class="well">'
. '<p>Login using your TestSwarm username and password.'
. ' If you don\'t have one you may <a href="' . swarmpath( "signup" )
. '">Signup Here</a>.</p>'
. '<p>Login for projects. Projects can only be created by swarm operators.</p>'
. '<div class="control-group">'
. '<label class="control-label" for="form-username">Username</label>'
. '<label class="control-label" for="form-projectID">Project ID</label>'
. '<div class="controls">'
. '<input type="text" name="username" required id="form-username" value="' . htmlspecialchars( $request->getVal( "username" ) ) . '">'
. '<input type="text" name="projectID" required id="form-projectID" value="' . htmlspecialchars( $request->getVal( "projectID" ) ) . '">'
. '</div>'
. '</div><div class="control-group">'
. '<label class="control-label" for="form-password">Password</label>'
. '<label class="control-label" for="form-projectPassword">Project password</label>'
. '<div class="controls">'
. '<input type="password" name="password" required id="form-password">'
. '<input type="password" name="projectPassword" required id="form-projectPassword">'
. '</div>'
. '</div>'
. '</div><div class="form-actions">'
@@ -25,49 +25,36 @@ protected function initContent() {
$data = $this->getAction()->getData();
$error = $this->getAction()->getError();

if ( !$error && $data['status'] == 'logged-out' ) {
if ( !$error ) {
$this->setTitle( 'Logged out!' );
$html .= html_tag( 'div', array( 'class' => 'alert alert-success' ),
'Thanks for running TestSwarm. You are now signed out.'
'You are now logged out.'
);
// Don't show form in case of success.
// Return early, we don't need to show the <form> in case of success.
return $html;
}

if ( $request->wasPosted() && $error ) {
$html .= html_tag( 'div', array( 'class' => 'alert alert-error' ), $error['info'] );
}


$html .= '<form action="' . swarmpath( 'logout' ) . '" method="post" class="form-horizontal">'
// If we weren't logged in in the first place, there is no error.
// If there is an error now it means we're on a POST request to Logout
// and the tokens were invalid. Likely a case of a malice or stale state (someone having
// a tab open for a month with absolute no other activity and the token no longer being
// valid when they finally click "Logout").
// Show the user the error and allow them user to manually confirm the log out.
$auth = $this->getContext()->getAuth();
$html .=
html_tag( 'div', array( 'class' => 'alert alert-error' ), $error['info'] )
. '<form action="' . swarmpath( 'logout' ) . '" method="post" class="form-horizontal">'
. '<fieldset>'
. '<legend>Log out</legend>'
. '<div class="form-actions">'
. '<p>Please submit the following protected form to proceed to log out.</p>'
. '<input type="submit" value="Logout" class="btn btn-primary">'
. self::getLogoutFormFieldsHtml( $this->getContext() )
. '<input type="submit" value="Logout" class="btn btn-primary">'
. '<input type="hidden" name="authID" value="' . htmlspecialchars( $auth->project->id ) . '">'
. '<input type="hidden" name="authToken" value="' . htmlspecialchars( $auth->sessionToken ) . '">'
. '</div>'
. '</fieldset>'
. '</form>';

return $html;
}

public static function getLogoutFormFieldsHtml( TestSwarmContext $context ) {
$db = $context->getDB();
$request = $context->getRequest();

$userName = $request->getSessionData( 'username' );
$userAuthToken = $db->getOne(str_queryf(
'SELECT auth
FROM users
WHERE name = %s',
$userName
));

return
'<input type="hidden" name="authUsername" value="' . htmlspecialchars( $userName ) . '">'
. '<input type="hidden" name="authToken" value="' . htmlspecialchars( $userAuthToken ) . '">';

}
}
@@ -0,0 +1,81 @@
<?php
/**
* "Project" page.
*
* @author John Resig, 2008-2011
* @author Jörn Zaefferer, 2012
* @author Timo Tijhof, 2012-2013
* @since 1.0.0
* @package TestSwarm
*/
class ProjectPage extends Page {
public function execute() {
$action = ProjectAction::newFromContext( $this->getContext() );
$action->doAction();

$this->setAction( $action );
$this->content = $this->initContent();
}

protected function initContent() {

$this->setTitle( "Project" );

$html = "";

$error = $this->getAction()->getError();
$data = $this->getAction()->getData();
if ( $error ) {
$html .= html_tag( "div", array( "class" => "alert alert-error" ), $error["info"] );
return $html;
}

$this->setSubTitle( $data['info']['display_title'] );

$info = array();
if ( $data['info']['site_url'] ) {
$info[] = 'Homepage: ' . html_tag( 'a', array( 'href' => $data['info']['site_url'] ), parse_url( $data['info']['site_url'], PHP_URL_HOST ) ?: $data['info']['site_url'] );
}
$info[] = 'Created: ' . self::getPrettyDateHtml( $data['info'], 'created' );
$info[] = 'Last updated: ' . self::getPrettyDateHtml( $data['info'], 'updated' );

$html .= '<div class="well well-small">' . implode( ' <span class="muted">|</span> ', $info ) . '</div>';

if ( !count( $data['jobs'] ) ) {

$html .= '<div class="alert alert-info">No jobs found.</div>';

} else {

$html .= '<h2>Jobs</h2><ul class="pager">';
if ( $data['pagination']['prev'] ) {
$html .= '<li class="previous">' . html_tag_open( 'a', array(
'href' => $data['pagination']['prev']['viewUrl'],
) ) . '&larr;&nbsp;Previous</a></li>';
} else {
$html .= '<li class="previous disabled" title="No previous page"><a href="#">&larr;&nbsp;Previous</a></span>';
}
if ( $data['pagination']['next'] ) {
$html .= '<li class="next">' . html_tag_open( 'a', array(
'href' => $data['pagination']['next']['viewUrl'],
) ) . 'Next&nbsp;&rarr;</a></li>';
} else {
$html .= '<li class="next disabled" title="No next page"><a href="#">Next&nbsp;&rarr;</a></span>';
}
$html .= '</ul>';

$html .= '<table class="table table-bordered swarm-results">';
$html .= '<thead>';
$html .= JobPage::getUaHtmlHeader( $data['userAgents'] );
$html .= '</thead><tbody>';

foreach ( $data['jobs'] as $job ) {
$html .= JobPage::getJobHtmlRow( $job, $data['userAgents'] );
}

$html .= '</tbody></table>';
}

return $html;
}
}
@@ -27,19 +27,30 @@ protected function initContent() {
. '<table class="table table-striped">'
. '<thead><tr>'
. '<th>Project name</th>'
. '<th class="span2">Jobs</th>'
. '<th class="span2">Most recent job</th>'
. '<th class="span4">Creation date</th>'
. '<th class="span4">Latest job</th>'
. '</tr></thead>'
. '<tbody>';

foreach ( $projects as $project ) {
$job = $project['job'];
$html .= '<tr>'
. '<td><a href="' . htmlspecialchars( swarmpath( "user/{$project['name']}" ) ) . '">' . htmlspecialchars( $project['name'] ) . '</a></td>'
. '<td class="num">' . htmlspecialchars( number_format( $project['jobCount'] ) ) . '</td>'
. '<td><a href="' . htmlspecialchars( swarmpath( "job/{$project['jobLatest']}" ) ) . '">Job #' . htmlspecialchars( $project['jobLatest'] ) . '</a></td>'
. '<td class="num">' . self::getPrettyDateHtml( $project, 'created' ) . '</td>'
. '</tr>';
. '<td><a href="'
. htmlspecialchars( swarmpath( "project/{$project['id']}" ) ) . '">'
. htmlspecialchars( $project['displayTitle'] ) . '</a></td>';
if ( !$job ) {
$html .= '<td>N/A</td>';
} else {
$html .= '<td class="swarm-status-cell swarm-jobstatus-cell"><div class="swarm-status swarm-status-' . $job['summary'] . '">'
. JobPage::getStatusIconHtml( $job['summary'] )
. html_tag( 'a', array(
'href' => $job['info']['viewUrl'],
'title' => $job['info']['nameText'],
), 'Job #' . $job['info']['id']
)
. '</div></td>';
}

$html .= '</tr>';
}
$html .= '</tbody></table>';

@@ -48,15 +48,15 @@ protected function initContent() {
return $html;
}

$this->setSubTitle( '#' . $data['resultInfo']['id'] );
$this->setSubTitle( '#' . $data['info']['id'] );


if ( $data['job'] ) {
$html = '<p><em>'
. html_tag_open( 'a', array( 'href' => $data['job']['url'], 'title' => 'Back to Job #' . $data['job']['id'] ) ) . '&laquo Back to Job #' . $data['job']['id'] . '</a>'
. '</em></p>';
} else {
$html = '<p><em>Run #' . $data['resultInfo']['runID'] . ' has been deleted. Job info unavailable.</em></p>';
$html = '<p><em>Run #' . $data['info']['runID'] . ' has been deleted. Job info unavailable.</em></p>';
}

if ( $data['otherRuns'] ) {
@@ -69,39 +69,38 @@ protected function initContent() {

$html .= '<h3>Information</h3>'
. '<table class="table table-striped">'
. '<colgroup><col class="span2"/><col/></colgroup>'
. '<tbody>'
. '<tr><th>Run</th><td>'
. ($data['job']
? html_tag( 'a', array( 'href' => $data['job']['url'] ), 'Job #' . $data['job']['id'] ) . ' / '
: ''
)
. 'Run #' . htmlspecialchars( $data['resultInfo']['runID'] )
. 'Run #' . htmlspecialchars( $data['info']['runID'] )
. '</td></tr>'
. '<tr><th>Client</th><td>'
. html_tag( 'a', array( 'href' => $data['client']['userUrl'] ), $data['client']['userName'] )
. ' / Client #' . htmlspecialchars( $data['resultInfo']['clientID'] )
. html_tag( 'a', array( 'href' => $data['client']['viewUrl'] ), 'Client #' . $data['info']['clientID'] )
. ' / ' . htmlspecialchars( $data['client']['name'] )
. '</td></tr>'
. '<tr><th>UA ID</th><td>'
. '<code>' . htmlspecialchars( $data['client']['uaID'] ) . '</code>'
. '<tr><th>User-Agent</th><td>'
. '<code>' . htmlspecialchars( $data['client']['userAgent'] ) . '</code>'
. '<tt>' . htmlspecialchars( $data['client']['uaRaw'] ) . '</tt>'
. '</td></tr>'
. '<tr><th>Run time</th><td>'
. ( isset( $data['resultInfo']['runTime'] )
? number_format( intval( $data['resultInfo']['runTime'] ) ) . 's'
. ( isset( $data['info']['runTime'] )
? number_format( intval( $data['info']['runTime'] ) ) . 's'
: '?'
)
. '</td></tr>'
. '<tr><th>Status</th><td>'
. htmlspecialchars( $data['resultInfo']['status'] )
. htmlspecialchars( $data['info']['status'] )
. '</td></tr>'
. '<tr><th>Started</th><td>'
. self::getPrettyDateHtml( $data['resultInfo'], 'started' )
. self::getPrettyDateHtml( $data['info'], 'started' )
. '</td></tr>'
. ( isset( $data['resultInfo']['savedLocalFormatted'] )
. ( isset( $data['info']['savedLocalFormatted'] )
? ('<tr><th>Saved</th><td>'
. self::getPrettyDateHtml( $data['resultInfo'], 'saved' )
. self::getPrettyDateHtml( $data['info'], 'saved' )
. '</td></tr>'
)
: ''
@@ -113,7 +112,7 @@ protected function initContent() {
. html_tag( 'a', array(
'href' => swarmpath( 'index.php' ) . '?' . http_build_query(array(
'action' => 'result',
'item' => $data['resultInfo']['id'],
'item' => $data['info']['id'],
'raw' => '',
)),
'target' => '_blank',
@@ -123,7 +122,7 @@ protected function initContent() {
. html_tag( 'iframe', array(
'src' => swarmpath( 'index.php' ) . '?' . http_build_query(array(
'action' => 'result',
'item' => $data['resultInfo']['id'],
'item' => $data['info']['id'],
'raw' => '',
)),
'width' => '100%',
@@ -164,7 +163,7 @@ protected function serveRawResults( $resultsID ) {
} else {
$this->outputMini(
'No Content',
'Client saved results but did not attach an HTML report.'
'Client saved results but did not attach an HTML report.'
);
}

@@ -14,21 +14,20 @@ protected function initContent() {
$conf = $this->getContext()->getConf();
$request = $this->getContext()->getRequest();

$uaData = $browserInfo->getUaData();
$this->setTitle( 'Test runner' );

$runToken = null;

if ( $conf->client->requireRunToken ) {
$runToken = $request->getVal( "run_token" );
if ( !$runToken ) {
throw new SwarmException( "This swarm has restricted access to join the swarm." );
return '<div class="alert alert-error">This swarm has restricted access to join the swarm.</div>';
}
}

$this->setTitle( "Test runner" );
$this->bodyScripts[] = swarmpath( "js/run.js?" . time() );

$client = Client::newFromContext( $this->getContext(), $runToken );
$displayInfo = $uaData->displayInfo;

$html = '<script>'
. 'SWARM.client_id = ' . json_encode( $client->getClientRow()->id ) . ';'
@@ -38,19 +37,10 @@ protected function initContent() {
$html .=
'<div class="row">'
. '<div class="span2">'
. '<div class="well well-swarm-icon">'
. html_tag( 'div', array(
'class' => $displayInfo['class'],
'title' => $displayInfo['title'],
) )
. '<br>'
. html_tag_open( 'span', array(
'class' => 'badge swarm-browsername',
) ) . $displayInfo['labelHtml'] . '</span>'
. '</div>'
. $browserInfo->getIconHtml()
. '</div>'
. '<div class="span7">'
. '<h2>' . htmlspecialchars( $client->getUserRow()->name ) . '</h2>'
. '<h2>' . htmlspecialchars( $client->getClientRow()->name ) . '</h2>'
. '<p><strong>Status:</strong> <span id="msg"></span></p>'
. '</div>'
. '</div>'

This file was deleted.

This file was deleted.

This file was deleted.

@@ -141,7 +141,7 @@ function str_queryf($string) {
$sql_query .= "('" . implode( "', '", $escapedList ) . "')";
break;
}
if ($char != 'x') {
if ( $char != 'x' ) {
$args_i++;
}
} else {
@@ -157,7 +157,7 @@ function str_queryf($string) {
* PHP has natsort() but no natksort().
*
* @source http://stackoverflow.com/a/1186347/319266
* @seealso php.net/uksort, php.net/natsort, php.net/strnatcmp
* @see php.net/uksort, php.net/natsort, php.net/strnatcmp
*/
function natksort( &$array ) {
uksort( $array, 'strnatcmp' );
@@ -168,7 +168,7 @@ function natksort( &$array ) {
* PHP has natcasesort() but no natcaseksort().
*
* @source http://stackoverflow.com/a/1186347/319266
* @seealso php.net/uksort, php.net/natcasesort, php.net/strnatcasecmp
* @see php.net/uksort, php.net/natcasesort, php.net/strnatcasecmp
*/
function natcaseksort( &$array ) {
uksort( $array, 'strnatcasecmp' );
@@ -72,8 +72,8 @@ jQuery(function ( $ ) {
run_id: $el.data( 'runId' ),
client_id: $el.data( 'clientId' ),
useragent_id: $el.data( 'useragentId' ),
authUsername: SWARM.user.name,
authToken: SWARM.user.authToken
authID: SWARM.auth.project.id,
authToken: SWARM.auth.sessionToken
},
dataType: 'json',
success: function ( data ) {
@@ -88,7 +88,7 @@ jQuery(function ( $ ) {

$( 'table.swarm-results' ).prev().before( $indicator );

if ( SWARM.user ) {
if ( SWARM.auth ) {

// This needs to bound as a delegate, because the table auto-refreshes.
$targetTable.on( 'click', '.swarm-reset-run-single', function () {
@@ -118,15 +118,15 @@ jQuery(function ( $ ) {
action: 'wipejob',
job_id: SWARM.jobInfo.id,
type: 'delete',
authUsername: SWARM.user.name,
authToken: SWARM.user.authToken
authID: SWARM.auth.project.id,
authToken: SWARM.auth.sessionToken
},
dataType: 'json',
success: function ( data ) {
if ( data.wipejob && data.wipejob.result === 'ok' ) {
// Right now the only user authorized to delete a job is the creator,
// the below code makes that assumption.
window.location.href = SWARM.conf.web.contextpath + 'user/' + SWARM.user.name;
window.location.href = SWARM.conf.web.contextpath + 'project/' + SWARM.auth.project.id;
return;
}
actionComplete();
@@ -153,8 +153,8 @@ jQuery(function ( $ ) {
action: 'wipejob',
job_id: SWARM.jobInfo.id,
type: 'reset',
authUsername: SWARM.user.name,
authToken: SWARM.user.authToken
authID: SWARM.auth.project.id,
authToken: SWARM.auth.sessionTokens
},
dataType: 'json',
success: function ( data ) {
@@ -7,26 +7,72 @@
* @package TestSwarm
*/
jQuery(function ( $ ) {
var query = {},
search = window.location.search;

// Skip leading '?'
if ( search.length > 1 ) {
$.each( search.slice( 1 ).split( '&' ), function ( i, parts ) {
parts = parts.replace( /^([^=]+)=(.*)$/, function ( p0, p1, p2 ) {
query[ decodeURIComponent( p1 ) ] = decodeURIComponent( p2 );
} );
});
}

if ( $.fn.prettyDate ) {
$( '.pretty' ).prettyDate();
}

if ( SWARM.user ) {
if ( SWARM.auth ) {
$( '.swarm-logout-link' ).on( 'click', function ( e ) {
$( '<form>', {
action: SWARM.conf.web.contextpath,
method: 'POST',
css: { display: 'none' }
})
.append(
$( '<input>', { type: 'hidden', name: 'action', value: 'logout' }),
$( '<input>', { type: 'hidden', name: 'authUsername', value: SWARM.user.name }),
$( '<input>', { type: 'hidden', name: 'authToken', value: SWARM.user.authToken })
$( '<input type="hidden"/>' ).prop({ name: 'action', value: 'logout' }),
$( '<input type="hidden"/>' ).prop({ name: 'authID', value: SWARM.auth.project.id }),
$( '<input type="hidden"/>' ).prop({ name: 'authToken', value: SWARM.auth.sessionToken })
)
.appendTo( 'body' )
.submit();

e.preventDefault();
});
}

$( '.swarm-form-join [name="item"]' ).each( function () {
var el = this;
$( el ).on( 'input change', function () {
if ( el.value && el.checkValidity && !el.checkValidity() && el.setCustomValidity ) {
// Override the error message that is displayed when the field is non-empty
// and didn't pass validation, defaults to "Did not match pattern" which is not
// useful as the user doesn't know the pattern.
el.setCustomValidity(
'Names should be no longer than 128 characters.'
);
} else {
el.setCustomValidity( '' );
}
});
$([ el, el.form ]).on( 'blur submit', function () {
if ( !el.value ) {
el.value = 'anonymous';
}
});
} );

$( document ).on( 'click', '.swarm-toggle', function () {
var key,
toggleQuery = $( this ).data( 'toggle-query' );
for ( key in toggleQuery ) {
if ( query[key] !== undefined && ( toggleQuery[key] === null || toggleQuery[key] === null ) ) {
delete query[key];
} else {
query[key] = toggleQuery[key];
}
}
window.location.search = '?' + $.param( query );
});
});
@@ -69,10 +69,10 @@ Note that any QUnit specific details here may out of date. Pay attention to the

### Authentication

Username with a user that will be the owner of this new job.
Project that will be the owner of this new job.

* `authUsername`: Matching entry from `users.name` field in the database
* `authToken`: Matching entry from `users.auth` field in the database
* `authID`: Matching entry from `projects.id` field in the database
* `authToken`: Matching entry from `projects.auth_token` field in the database

### Job information

@@ -31,6 +31,8 @@ protected function execute() {
$this->getContext()->dbLock( true );

$dbTables = array(
// Order matters (due to foreign key restrictions before 1.0)
'projects', // New in 1.0.0
'runresults', // New in 1.0.0
'run_client', // Removed in 1.0.0
'clients',
@@ -39,8 +39,11 @@ protected function execute() {
return;
}

$this->out( 'From which version are you upgrading? (use --quick to skip this)' );
$this->out( 'From which version are you upgrading? (leave empty or use --quick option to skip this)' );
$originVersion = $this->cliInput();
if ( !$originVersion ) {
$originVersion = '(auto...)';
}

// 1 => 1.0.0
$originVersion = explode( '.', $originVersion );
@@ -102,21 +105,22 @@ protected function doDatabaseUpdates() {
$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...' );
$this->out( 'Running tests on the database to detect which updates are needed.' );

/**
* 1.0.0
* 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
// (it does mostly migrate the users table).
// (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 ( !is_object( $clients_useragent_id ) ) {
if ( !$clients_useragent_id ) {
$this->unknownDatabaseState( 'clients.useragent_id not found' );
return;
}
@@ -125,15 +129,15 @@ protected function doDatabaseUpdates() {
&& !$clients_useragent_id->numeric
&& $clients_useragent_id->type === 'string'
) {
$this->out( '...run_client already dropped' );
$this->out( '...users.request already dropped' );
$this->out( '...client.useragent_id is up to date' );
$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 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)' );
. "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
@@ -148,8 +152,7 @@ protected function doDatabaseUpdates() {

// Drop all known TestSwarm tables in the database
// (except users, handled separately)
foreach( array(
'runresults', // New in 1.0.0
foreach ( array(
'run_client', // Removed in 1.0.0
'clients',
'run_useragent',
@@ -204,17 +207,17 @@ protected function doDatabaseUpdates() {
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.' );
$this->out( 'SKIPPED: Not a project 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'_
// 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
// 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();
@@ -226,7 +229,6 @@ protected function doDatabaseUpdates() {
SET
auth = %s
WHERE id = %u',
// authToken is used in addjob scripts.
$userRow->auth,
$data['userID']
));
@@ -240,7 +242,218 @@ protected function doDatabaseUpdates() {
}

} // End of users re-import
} // End of 1.0.0-alpha update
} // 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` (
`id` varchar(255) binary NOT NULL PRIMARY KEY,
`display_title` varchar(255) binary NOT NULL,
`site_url` blob NOT NULL default '',
`password` tinyblob NOT NULL,
`auth_token` tinyblob NOT NULL,
`updated` binary(14) NOT NULL,
`created` binary(14) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
);

$this->out( '... adding clients.name' );
$db->query(
"ALTER TABLE clients
ADD `name` varchar(255) binary NOT NULL AFTER `id`"
);

$this->out( '... adding jobs.project_id' );
$db->query(
"ALTER TABLE jobs
ADD `project_id` varchar(255) binary NOT NULL AFTER `name`"
);

$this->out( '... dropping constraint fk_clients_user_id' );
$db->query(
"ALTER TABLE clients
DROP FOREIGN KEY fk_clients_user_id"
);

$this->out( '... dropping constraint fk_jobs_user_id' );
$db->query(
"ALTER TABLE jobs
DROP FOREIGN KEY fk_jobs_user_id"
);

$this->out( '... dropping constraint fk_runs_job_id' );
$db->query(
"ALTER TABLE runs
DROP FOREIGN KEY fk_runs_job_id"
);

$this->out( '... dropping constraint fk_run_useragent_run_id' );
$db->query(
"ALTER TABLE run_useragent
DROP FOREIGN KEY fk_run_useragent_run_id"
);

$this->out( '... dropping constraint fk_runresults_client_id' );
$db->query(
"ALTER TABLE runresults
DROP FOREIGN KEY fk_runresults_client_id"
);

$this->out( '... dropping index idx_users_name' );
$db->query(
"ALTER TABLE users
DROP INDEX idx_users_name"
);

$this->out( '... dropping index idx_clients_user_useragent_updated' );
$db->query(
"ALTER TABLE clients
DROP INDEX idx_clients_user_useragent_updated"
);

$this->out( '... dropping index idx_jobs_user' );
$db->query(
"ALTER TABLE jobs
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->query(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
CHANGE COLUMN `useragent_id` `useragent_id` varchar(255) NOT NULL"
);

$this->out( '... dropping clients.user_id' );
$db->query(
"ALTER TABLE clients
DROP COLUMN `user_id`"
);

$this->out( '... dropping jobs.user_id' );
$db->query(
"ALTER TABLE jobs
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
ADD INDEX idx_clients_name_ua_created (name, useragent_id, created);" );

$this->out( '... adding index idx_jobs_project_created' );
$db->query(
"ALTER TABLE jobs
ADD INDEX idx_jobs_project_created (project_id, created);" );


} // End of patch-users-projects-conversion.sql


$this->getContext()->dbLock( false );
@@ -249,10 +462,12 @@ protected function doDatabaseUpdates() {

protected function unknownDatabaseState( $error = '' ) {
if ( $error !== '' ) {
$error = "\nError: $error";
$error = "\n\nLast failed check before abort: $error";
}
$this->error( "The database was found in a state not known in any version.\n"
."Please verify your settings. Note that this is not an installer! $error" );
$this->error( "\nThe database was found in a state not known in any version.\n"
. "This could be the result of a previous update run being aborted mid-update,\n"
. "in that case the automatic update script cannot help you.\n"
. "Please verify your local settings. Note that this is not an installer! $error" );
}
}

@@ -0,0 +1,139 @@
<?php
/**
* manageProject.php
*
* @author Timo Tijhof, 2012
* @since 1.0.0
* @package TestSwarm
*/
define( 'SWARM_ENTRY', 'SCRIPT' );
require_once __DIR__ . '/../inc/init.php';

class ManageProjectScript extends MaintenanceScript {

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: [a-z][-a-z0-9], max: 255 chars).' );
$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 execute() {
$create = $this->getOption( 'create' );

if ( $create ) {
$this->create();
} else {
$this->update();

}
}

protected function create() {
$action = ProjectAction::newFromContext( $this->getContext() );

$id = $this->getOption( 'id' );
$displayTitle = $this->getOption( 'display-title' );
$password = $this->getOption( 'password' );
$siteUrl = $this->getOption( 'site-url' );

if ( !$id || !$displayTitle ) {
$this->error( '--id and --display-title are required.' );
}

if ( !$password ) {
$inputConfirm = null;
$this->out( 'Enter password for this project (leave blank to abort):' );
while ( ( $input = $this->cliInputSecret() ) !== '' && $input !== $inputConfirm ) {
if ( !is_string( $inputConfirm ) ) {
$inputConfirm = $input;
$this->out( 'Re-enter password to confirm:' );
} else {
$inputConfirm = null;
$this->out( 'Passwords don\'t match, please try again:' );
}
}
if ( $input === '' ) {
$this->error( 'Password is required.' );
}
$password = $input;
}

$data = $action->create( $id, array(
'password' => $password,
'displayTitle' => $displayTitle,
'siteUrl' => $siteUrl,
) ) ;
$error = $action->getError();

if ( $error ) {
$this->error( $error['info'] );
}

$this->out(
'Project ' . $displayTitle . ' has been succesfully created!' . PHP_EOL
. 'The following auth token has been generated for this project:' . PHP_EOL
. PHP_EOL
. "\t" . $data['authToken'] . PHP_EOL
. PHP_EOL
. 'You will need it to perform actions that require authentication.' . PHP_EOL
. 'If you ever loose it, you can generate a new token with the refreshProjectToken.php script.'
);
}

protected function update() {
$db = $this->getContext()->getDB();


$id = $this->getOption( 'id' );
$displayTitle = $this->getOption( 'display-title' );
$siteUrl = $this->getOption( 'site-url' );

if ( !$id ) {
$this->error( '--id is required.' );
}

// Check if this project exists.
$field = $db->getOne(str_queryf( 'SELECT id FROM projects WHERE id = %s;', $id ));
if ( !$field ) {
$this->error( 'Project does not exist. Set --create to create a project.' );
}

if ( !$displayTitle && !$siteUrl ) {
$this->error( 'Unable to perform update. No values provided.' );
}

if ( $displayTitle ) {
$isUpdated = $db->query(str_queryf(
'UPDATE projects SET display_title = %s, updated = %s WHERE id = %s;',
$displayTitle,
swarmdb_dateformat( SWARM_NOW ),
$id
));
if ( !$isUpdated ) {
$this->error( 'Failed to update database.' );
}
}

if ( $siteUrl ) {
$isUpdated = $db->query(str_queryf(
'UPDATE projects SET site_url = %s, updated = %s WHERE id = %s;',
$siteUrl,
swarmdb_dateformat( SWARM_NOW ),
$id
));
if ( !$isUpdated ) {
$this->error( 'Failed to update database.' );
}
}

$this->out( 'Project has been updated.' );
}
}

$script = ManageProjectScript::newFromContext( $swarmContext );
$script->run();
@@ -31,8 +31,8 @@ protected function execute() {
$findUnknown = $this->getOption( 'find-unknown' );
$replaceUnknown = $this->getOption( 'replace-unknown' );
if ( $findUnknown || $replaceUnknown ) {
$found1 = $this->findUnknown( 'clients', $batchSize, $replaceUnknown );
$found2 = $this->findUnknown( 'run_useragent', $batchSize, $replaceUnknown );
$found1 = $this->findUnknown( 'clients', $batchSize, $replaceUnknown );
$found2 = $this->findUnknown( 'run_useragent', $batchSize, $replaceUnknown );
if ( !$replaceUnknown ) {
$found = array_values( array_unique( array_merge( $found1, $found2 ) ) );
natsort( $found );
@@ -91,7 +91,7 @@ protected function runBatchTable( $table, $fromId, $toId, $batchSize ) {
"UPDATE $table
SET useragent_id = %s
WHERE id BETWEEN $blockStart AND $blockEnd
AND useragent_id = %s;
AND useragent_id = %s;
",
$toId,
$fromId