diff --git a/.gitignore b/.gitignore index d8c5ef7b..fbfc1a28 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,8 @@ config.php PHPCI/config.yml cache /loggerconfig.php -/pluginconfig.php \ No newline at end of file +/pluginconfig.php +PHPCI/Model/Migration.php +PHPCI/Model/Base/MigrationBase.php +PHPCI/Store/MigrationStore.php +PHPCI/Store/Base/MigrationStoreBase.php diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f0c8b605 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:14.04 +MAINTAINER Dan Cryer dan.cryer@block8.co.uk + +RUN echo "deb http://ppa.launchpad.net/ondrej/php5/ubuntu trusty main" >> /etc/apt/sources.list +RUN echo "deb http://archive.ubuntu.com/ubuntu/ precise universe" >> /etc/apt/sources.list +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C E5267A6C 0xcbcb082a1bb943db +RUN apt-get update + +# Install PHP: +RUN apt-get install -qy git-core php5-common php5-cli php5-curl php5-imap php5-mcrypt php5-mysqlnd + +# Give Git some fake user details to prevent it asking when trying to test merges: +RUN git config --global user.name "PHPCI" +RUN git config --global user.email "hello@php.ci" + +ADD ./ /phpci + +CMD /phpci/daemonise phpci:daemonise diff --git a/LICENSE.md b/LICENSE.md index 7d8d2f31..1df52b9c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2013, Block 8 Limited +Copyright (c) 2013-2014, Block 8 Limited All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/PHPCI/Application.php b/PHPCI/Application.php index ecc71ad5..3e15686e 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -2,14 +2,15 @@ /** * PHPCI - Continuous Integration for PHP * -* @copyright Copyright 2013, Block 8 Limited. +* @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md -* @link http://www.phptesting.org/ +* @link https://www.phptesting.org/ */ namespace PHPCI; use b8; +use b8\Exception\HttpException; use b8\Http\Response; use b8\Http\Response\RedirectResponse; use b8\View; @@ -51,6 +52,7 @@ class Application extends b8\Application $response->setResponseCode(401); $response->setContent(''); } else { + $_SESSION['login_redirect'] = substr($request->getPath(), 1); $response = new RedirectResponse($response); $response->setHeader('Location', PHPCI_URL.'session/login'); } @@ -69,7 +71,25 @@ class Application extends b8\Application */ public function handleRequest() { - $this->response = parent::handleRequest(); + try { + $this->response = parent::handleRequest(); + } catch (HttpException $ex) { + $this->config->set('page_title', 'Error'); + + $view = new View('exception'); + $view->exception = $ex; + + $this->response->setResponseCode($ex->getErrorCode()); + $this->response->setContent($view->render()); + } catch (\Exception $ex) { + $this->config->set('page_title', 'Error'); + + $view = new View('exception'); + $view->exception = $ex; + + $this->response->setResponseCode(500); + $this->response->setContent($view->render()); + } if (View::exists('layout') && $this->response->hasLayout()) { $view = new View('layout'); diff --git a/PHPCI/BuildFactory.php b/PHPCI/BuildFactory.php index 82ebc221..4825047d 100644 --- a/PHPCI/BuildFactory.php +++ b/PHPCI/BuildFactory.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * -* @copyright Copyright 2013, Block 8 Limited. +* @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md -* @link http://www.phptesting.org/ +* @link https://www.phptesting.org/ */ namespace PHPCI; @@ -21,11 +21,16 @@ class BuildFactory /** * @param $buildId * @return Build + * @throws \Exception */ public static function getBuildById($buildId) { $build = Factory::getStore('Build')->getById($buildId); + if (empty($build)) { + throw new \Exception('Build ID ' . $buildId . ' does not exist.'); + } + return self::getBuild($build); } diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index ccb78c6b..674e63a9 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -2,15 +2,14 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI; use PHPCI\Helper\BuildInterpolator; -use PHPCI\Helper\CommandExecutor; use PHPCI\Helper\MailerFactory; use PHPCI\Logging\BuildLogger; use PHPCI\Model\Build; @@ -118,7 +117,12 @@ class Builder implements LoggerAwareInterface $pluginFactory->addConfigFromFile(PHPCI_DIR . "/pluginconfig.php"); $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger); - $this->commandExecutor = new CommandExecutor( + $executorClass = 'PHPCI\Helper\UnixCommandExecutor'; + if (IS_WIN) { + $executorClass = 'PHPCI\Helper\WindowsCommandExecutor'; + } + + $this->commandExecutor = new $executorClass( $this->buildLogger, PHPCI_DIR, $this->quiet, @@ -126,7 +130,6 @@ class Builder implements LoggerAwareInterface ); $this->interpolator = new BuildInterpolator(); - } /** @@ -242,7 +245,7 @@ class Builder implements LoggerAwareInterface */ public function executeCommand() { - return $this->commandExecutor->buildAndExecuteCommand(func_get_args()); + return $this->commandExecutor->executeCommand(func_get_args()); } /** diff --git a/PHPCI/Command/CreateAdminCommand.php b/PHPCI/Command/CreateAdminCommand.php index 647dffca..30d800b1 100644 --- a/PHPCI/Command/CreateAdminCommand.php +++ b/PHPCI/Command/CreateAdminCommand.php @@ -1,11 +1,11 @@ /dev/null 2>&1 & -* -* @copyright Copyright 2013, Block 8 Limited. -* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md -* @link http://www.phptesting.org/ -*/ + * PHPCI - Continuous Integration for PHP + * + * @copyright Copyright 2014, Block 8 Limited. + * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md + * @link https://www.phptesting.org/ + */ namespace PHPCI\Command; diff --git a/PHPCI/Command/DaemoniseCommand.php b/PHPCI/Command/DaemoniseCommand.php index f8cbc45b..2850ff5f 100644 --- a/PHPCI/Command/DaemoniseCommand.php +++ b/PHPCI/Command/DaemoniseCommand.php @@ -1,12 +1,11 @@ /dev/null 2>&1 & -* -* @copyright Copyright 2013, Block 8 Limited. -* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md -* @link http://www.phptesting.org/ -*/ + * PHPCI - Continuous Integration for PHP + * + * @copyright Copyright 2014, Block 8 Limited. + * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md + * @link https://www.phptesting.org/ + */ namespace PHPCI\Command; @@ -76,6 +75,7 @@ class DaemoniseCommand extends Command $this->run = true; $this->sleep = 0; $runner = new RunCommand($this->logger); + $runner->setBaxBuilds(1); $emptyInput = new ArgvInput(array()); diff --git a/PHPCI/Command/GenerateCommand.php b/PHPCI/Command/GenerateCommand.php index 44a499de..07307686 100644 --- a/PHPCI/Command/GenerateCommand.php +++ b/PHPCI/Command/GenerateCommand.php @@ -1,11 +1,11 @@ 'PHPCI'], ['default' => PHPCI_DIR], false); + $gen = new CodeGenerator( + Database::getConnection(), + array('default' => 'PHPCI'), + array('default' => PHPCI_DIR), + false + ); + $gen->generateModels(); $gen->generateStores(); } diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index aac34dca..83c50437 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Command; @@ -204,19 +204,7 @@ class InstallCommand extends Command { $output->write('Setting up your database... '); - // Load PHPCI's bootstrap file: - require(PHPCI_DIR . 'bootstrap.php'); - - try { - // Set up the database, based on table data from the models: - $gen = new Database\Generator(Database::getConnection(), 'PHPCI', './PHPCI/Model/Base/'); - $gen->generate(); - } catch (Exception $ex) { - $output->writeln(''); - $output->writeln('PHPCI failed to set up the database.'); - $output->writeln('' . $ex->getMessage() . ''); - die; - } + shell_exec(PHPCI_DIR . 'vendor/bin/phinx migrate -c "' . PHPCI_DIR . 'phinx.php"'); $output->writeln('OK'); } diff --git a/PHPCI/Command/PollCommand.php b/PHPCI/Command/PollCommand.php index 503e83a8..76ea1dfb 100644 --- a/PHPCI/Command/PollCommand.php +++ b/PHPCI/Command/PollCommand.php @@ -1,11 +1,10 @@ logger = $logger; } - protected function configure() { $this @@ -69,7 +73,7 @@ class RunCommand extends Command // For verbose mode we want to output all informational and above // messages to the symphony output interface. - if ($input->getOption('verbose')) { + if ($input->hasOption('verbose')) { $this->logger->pushHandler( new OutputLogHandler($this->output, Logger::INFO) ); @@ -79,7 +83,7 @@ class RunCommand extends Command $this->logger->addInfo("Finding builds to process"); $store = Factory::getStore('Build'); - $result = $store->getByStatus(0); + $result = $store->getByStatus(0, $this->maxBuilds); $this->logger->addInfo(sprintf("Found %d builds", count($result['items']))); $builds = 0; @@ -113,4 +117,9 @@ class RunCommand extends Command return $builds; } + + public function setBaxBuilds($numBuilds) + { + $this->maxBuilds = (int)$numBuilds; + } } diff --git a/PHPCI/Command/UpdateCommand.php b/PHPCI/Command/UpdateCommand.php index 127b272e..1b8becb1 100644 --- a/PHPCI/Command/UpdateCommand.php +++ b/PHPCI/Command/UpdateCommand.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Command; @@ -50,11 +50,9 @@ class UpdateCommand extends Command { $this->verifyInstalled($output); - $output->writeln('Updating PHPCI database.'); + $output->write('Updating PHPCI database: '); - // Update the database: - $gen = new \b8\Database\Generator(\b8\Database::getConnection(), 'PHPCI', './PHPCI/Model/Base/'); - $gen->generate(); + shell_exec(PHPCI_DIR . 'vendor/bin/phinx migrate -c "' . PHPCI_DIR . 'phinx.php"'); $output->writeln('Done!'); } diff --git a/PHPCI/Controller.php b/PHPCI/Controller.php index 442d9248..8b5cb806 100644 --- a/PHPCI/Controller.php +++ b/PHPCI/Controller.php @@ -1,4 +1,11 @@ view->plugins = $this->getUiPlugins(); $this->view->build = $build; $this->view->data = $this->getBuildData($build); @@ -95,7 +105,6 @@ class BuildController extends \PHPCI\Controller $data = array(); $data['status'] = (int)$build->getStatus(); $data['log'] = $this->cleanLog($build->getLog()); - $data['plugins'] = json_decode($build->getPlugins(), true); $data['created'] = !is_null($build->getCreated()) ? $build->getCreated()->format('Y-m-d H:i:s') : null; $data['started'] = !is_null($build->getStarted()) ? $build->getStarted()->format('Y-m-d H:i:s') : null; $data['finished'] = !is_null($build->getFinished()) ? $build->getFinished()->format('Y-m-d H:i:s') : null; @@ -110,6 +119,10 @@ class BuildController extends \PHPCI\Controller { $copy = BuildFactory::getBuildById($buildId); + if (empty($copy)) { + throw new NotFoundException('Build with ID: ' . $buildId . ' does not exist.'); + } + $build = new Build(); $build->setProjectId($copy->getProjectId()); $build->setCommitId($copy->getCommitId()); @@ -134,11 +147,10 @@ class BuildController extends \PHPCI\Controller throw new \Exception('You do not have permission to do that.'); } - $build = BuildFactory::getBuildById($buildId); + $build = BuildFactory::getBuildById($buildId); - if (!$build) { - $this->response->setResponseCode(404); - return '404 - Not Found'; + if (empty($build)) { + throw new NotFoundException('Build with ID: ' . $buildId . ' does not exist.'); } $this->buildStore->delete($build); diff --git a/PHPCI/Controller/BuildStatusController.php b/PHPCI/Controller/BuildStatusController.php index 74a1fbad..54132401 100644 --- a/PHPCI/Controller/BuildStatusController.php +++ b/PHPCI/Controller/BuildStatusController.php @@ -1,11 +1,11 @@ projectStore->getById($projectId); - if (!$project) { + + if (empty($project)) { throw new NotFoundException('Project with id: ' . $projectId . ' not found'); } diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index dc4027ad..a4f8003d 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -1,11 +1,11 @@ canInstall = function_exists('shell_exec'); - - if ($this->canInstall) { - $this->composerPath = $this->findBinary(array('composer', 'composer.phar')); - - if (!$this->composerPath) { - $this->canInstall = false; - } - } - } - public function index() { if (!$_SESSION['user']->getIsAdmin()) { @@ -58,7 +44,6 @@ class PluginController extends \PHPCI\Controller } $this->view->canWrite = is_writable(APPLICATION_PATH . 'composer.json'); - $this->view->canInstall = $this->canInstall; $this->view->required = $this->required; $json = $this->getComposerJson(); @@ -93,13 +78,6 @@ class PluginController extends \PHPCI\Controller unset($json['require'][$package]); $this->setComposerJson($json); - if ($this->canInstall) { - $home = 'COMPOSER_HOME='.APPLICATION_PATH . ' '; - $action = ' update --working-dir=' . APPLICATION_PATH; - $toLog = APPLICATION_PATH . '/phpci_composer_remove.log 2>&1 &'; - shell_exec($home . $this->composerPath . $action . ' > /' . $toLog); - } - header('Location: ' . PHPCI_URL . 'plugin?r=' . $package); die; } @@ -121,16 +99,6 @@ class PluginController extends \PHPCI\Controller $json['require'][$package] = $version; $this->setComposerJson($json); - if ($this->canInstall) { - $home = 'COMPOSER_HOME='.APPLICATION_PATH . ' '; - $action = ' update --working-dir=' . APPLICATION_PATH; - $toLog = ' > /' . APPLICATION_PATH . '/phpci_composer_install.log 2>&1 &'; - shell_exec($home . $this->composerPath . $action . $toLog); - - header('Location: ' . PHPCI_URL . 'plugin?i=' . $package); - die; - } - header('Location: ' . PHPCI_URL . 'plugin?w=' . $package); die; } @@ -179,7 +147,7 @@ class PluginController extends \PHPCI\Controller { $searchQuery = $this->getParam('q', ''); $http = new \b8\HttpClient(); - $http->setHeaders(array('User-Agent: PHPCI/1.0 (+http://www.phptesting.org)')); + $http->setHeaders(array('User-Agent: PHPCI/1.0 (+https://www.phptesting.org)')); $res = $http->get('https://packagist.org/search.json', array('q' => $searchQuery)); die(json_encode($res['body'])); @@ -189,7 +157,7 @@ class PluginController extends \PHPCI\Controller { $name = $this->getParam('p', ''); $http = new \b8\HttpClient(); - $http->setHeaders(array('User-Agent: PHPCI/1.0 (+http://www.phptesting.org)')); + $http->setHeaders(array('User-Agent: PHPCI/1.0 (+https://www.phptesting.org)')); $res = $http->get('https://packagist.org/packages/'.$name.'.json'); die(json_encode($res['body'])); diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index eea5a7a8..76f794c7 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -1,25 +1,25 @@ buildStore = Store\Factory::getStore('Build'); - $this->projectStore = Store\Factory::getStore('Project'); + $this->buildStore = Store\Factory::getStore('Build'); + $this->projectStore = Store\Factory::getStore('Project'); } /** @@ -51,17 +51,18 @@ class ProjectController extends \PHPCI\Controller public function view($projectId) { $project = $this->projectStore->getById($projectId); - if (!$project) { + + if (empty($project)) { throw new NotFoundException('Project with id: ' . $projectId . ' not found'); } - $page = $this->getParam('p', 1); - $builds = $this->getLatestBuildsHtml($projectId, (($page - 1) * 10)); + $page = $this->getParam('p', 1); + $builds = $this->getLatestBuildsHtml($projectId, (($page - 1) * 10)); - $this->view->builds = $builds[0]; - $this->view->total = $builds[1]; - $this->view->project = $project; - $this->view->page = $page; + $this->view->builds = $builds[0]; + $this->view->total = $builds[1]; + $this->view->project = $project; + $this->view->page = $page; $this->config->set('page_title', $project->getTitle()); @@ -76,6 +77,10 @@ class ProjectController extends \PHPCI\Controller /* @var \PHPCI\Model\Project $project */ $project = $this->projectStore->getById($projectId); + if (empty($project)) { + throw new NotFoundException('Project with id: ' . $projectId . ' not found'); + } + $build = new Build(); $build->setProjectId($projectId); $build->setCommitId('Manual'); @@ -96,7 +101,7 @@ class ProjectController extends \PHPCI\Controller public function delete($projectId) { if (!$_SESSION['user']->getIsAdmin()) { - throw new \Exception('You do not have permission to do that.'); + throw new ForbiddenException('You do not have permission to do that.'); } $project = $this->projectStore->getById($projectId); @@ -120,10 +125,10 @@ class ProjectController extends \PHPCI\Controller */ protected function getLatestBuildsHtml($projectId, $start = 0) { - $criteria = array('project_id' => $projectId); - $order = array('id' => 'DESC'); - $builds = $this->buildStore->getWhere($criteria, 10, $start, array(), $order); - $view = new b8\View('BuildsTable'); + $criteria = array('project_id' => $projectId); + $order = array('id' => 'DESC'); + $builds = $this->buildStore->getWhere($criteria, 10, $start, array(), $order); + $view = new b8\View('BuildsTable'); foreach ($builds['items'] as &$build) { $build = BuildFactory::getBuild($build); @@ -142,7 +147,7 @@ class ProjectController extends \PHPCI\Controller $this->config->set('page_title', 'Add Project'); if (!$_SESSION['user']->getIsAdmin()) { - throw new \Exception('You do not have permission to do that.'); + throw new ForbiddenException('You do not have permission to do that.'); } $method = $this->request->getMethod(); @@ -173,27 +178,17 @@ class ProjectController extends \PHPCI\Controller $values = $form->getValues(); - if ($values['type'] == "gitlab") { - preg_match('`^(.*)@(.*):(.*)/(.*)\.git`', $values['reference'], $matches); - + $matches = array(); + if ($values['type'] == "gitlab" && preg_match('`^(.*)@(.*):(.*)/(.*)\.git`', $values['reference'], $matches)) { $info = array(); - if (isset($matches[1])) { - $info["user"] = $matches[1]; - } - - if (isset($matches[2])) { - $info["domain"] = $matches[2]; - } - + $info['user'] = $matches[1]; + $info['domain'] = $matches[2]; $values['access_information'] = serialize($info); - - if (isset($matches[3]) && isset($matches[4])) { - $values['reference'] = $matches[3]."/".$matches[4]; - } + $values['reference'] = $matches[3]."/".$matches[4]; } - $values['git_key'] = $values['key']; - $values['public_key'] = $values['pubkey']; + $values['ssh_private_key'] = $values['key']; + $values['ssh_public_key'] = $values['pubkey']; $project = new Project(); $project->setValues($values); @@ -210,31 +205,33 @@ class ProjectController extends \PHPCI\Controller public function edit($projectId) { if (!$_SESSION['user']->getIsAdmin()) { - throw new \Exception('You do not have permission to do that.'); + throw new ForbiddenException('You do not have permission to do that.'); } - $method = $this->request->getMethod(); - $project = $this->projectStore->getById($projectId); + $method = $this->request->getMethod(); + $project = $this->projectStore->getById($projectId); + + if (empty($project)) { + throw new NotFoundException('Project with id: ' . $projectId . ' not found'); + } $this->config->set('page_title', 'Edit: ' . $project->getTitle()); + $values = $project->getDataArray(); + $values['key'] = $values['ssh_private_key']; + $values['pubkey'] = $values['ssh_public_key']; + + if ($values['type'] == "gitlab") { + $accessInfo = $project->getAccessInformation(); + $reference = $accessInfo["user"].'@'.$accessInfo["domain"].':' . $project->getReference().".git"; + $values['reference'] = $reference; + } if ($method == 'POST') { $values = $this->getParams(); - } else { - $values = $project->getDataArray(); - $values['key'] = $values['git_key']; - $values['pubkey'] = $values['public_key']; - - if ($values['type'] == "gitlab") { - $accessInfo = $project->getAccessInformation(); - $reference = $accessInfo["user"].'@'.$accessInfo["domain"].':' . $project->getReference().".git"; - $values['reference'] = $reference; - } } - - $form = $this->projectForm($values, 'edit/' . $projectId); + $form = $this->projectForm($values, 'edit/' . $projectId); if ($method != 'POST' || ($method == 'POST' && !$form->validate())) { $view = new b8\View('ProjectForm'); @@ -247,8 +244,8 @@ class ProjectController extends \PHPCI\Controller } $values = $form->getValues(); - $values['git_key'] = $values['key']; - $values['public_key'] = $values['pubkey']; + $values['ssh_private_key'] = $values['key']; + $values['ssh_public_key'] = $values['pubkey']; if ($values['type'] == "gitlab") { preg_match('`^(.*)@(.*):(.*)/(.*)\.git`', $values['reference'], $matches); diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index 15563b19..4b4f4fc9 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -1,15 +1,16 @@ getParam('password', ''), $user->getHash())) { $_SESSION['user_id'] = $user->getId(); - header('Location: ' . PHPCI_URL); + header('Location: ' . $this->getLoginRedirect()); die; } else { $isLoginFailure = true; @@ -88,4 +89,86 @@ class SessionController extends \PHPCI\Controller header('Location: ' . PHPCI_URL); die; } + + public function forgotPassword() + { + if ($this->request->getMethod() == 'POST') { + $email = $this->getParam('email', null); + $user = $this->userStore->getByEmail($email); + + if (empty($user)) { + $this->view->error = 'No user exists with that email address, please try again.'; + return $this->view->render(); + } + + $key = md5(date('Y-m-d') . $user->getHash()); + $url = PHPCI_URL; + $name = $user->getName(); + $userId = $user->getId(); + + $message = <<setEmailTo($user->getEmail(), $user->getName()); + $email->setSubject('Password reset'); + $email->setBody($message); + $email->send(); + + $this->view->emailed = true; + } + + return $this->view->render(); + } + + public function resetPassword($userId, $key) + { + $user = $this->userStore->getById($userId); + $userKey = md5(date('Y-m-d') . $user->getHash()); + + if (empty($user) || $key != $userKey) { + $this->view->error = 'Invalid password reset request.'; + return $this->view->render(); + } + + if ($this->request->getMethod() == 'POST') { + $hash = password_hash($this->getParam('password'), PASSWORD_DEFAULT); + $user->setHash($hash); + + $_SESSION['user'] = $this->userStore->save($user); + $_SESSION['user_id'] = $user->getId(); + + header('Location: ' . PHPCI_URL); + die; + } + + $this->view->id = $userId; + $this->view->key = $key; + + return $this->view->render(); + } + + protected function getLoginRedirect() + { + $rtn = PHPCI_URL; + + if (!empty($_SESSION['login_redirect'])) { + $rtn .= $_SESSION['login_redirect']; + $_SESSION['login_redirect'] = null; + } + + return $rtn; + } } diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index 2e0fc5ec..ebcab7e8 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Controller; @@ -48,6 +48,7 @@ class SettingsController extends Controller $this->view->github = $this->getGithubForm(); $this->view->emailSettings = $this->getEmailForm($emailSettings); + $this->view->isWriteable = $this->canWriteConfig(); if (!empty($this->settings['phpci']['github']['token'])) { $this->view->githubUser = $this->getGithubUser($this->settings['phpci']['github']['token']); @@ -242,4 +243,9 @@ class SettingsController extends Controller return $user['body']; } + + protected function canWriteConfig() + { + return is_writeable(APPLICATION_PATH . 'PHPCI/config.yml'); + } } diff --git a/PHPCI/Controller/UserController.php b/PHPCI/Controller/UserController.php index c8fade0c..86fa8662 100644 --- a/PHPCI/Controller/UserController.php +++ b/PHPCI/Controller/UserController.php @@ -1,15 +1,17 @@ userStore = b8\Store\Factory::getStore('User'); + $this->userStore = b8\Store\Factory::getStore('User'); } /** @@ -45,18 +47,72 @@ class UserController extends Controller return $this->view->render(); } + public function profile() + { + $user = $_SESSION['user']; + $values = $user->getDataArray(); + + if ($this->request->getMethod() == 'POST') { + $values = $this->getParams(); + + if (!empty($values['password'])) { + $values['hash'] = password_hash($values['password'], PASSWORD_DEFAULT); + } + + $this->view->updated = true; + + $user->setValues($values); + $_SESSION['user'] = $this->userStore->save($user); + } + + $form = new Form(); + $form->setAction(PHPCI_URL.'user/profile'); + $form->setMethod('POST'); + + $name = new Form\Element\Text('name'); + $name->setClass('form-control'); + $name->setContainerClass('form-group'); + $name->setLabel('Name'); + $name->setRequired(true); + $form->addField($name); + + $email = new Form\Element\Email('email'); + $email->setClass('form-control'); + $email->setContainerClass('form-group'); + $email->setLabel('Email Address'); + $email->setRequired(true); + $form->addField($email); + + $password = new Form\Element\Password('password'); + $password->setClass('form-control'); + $password->setContainerClass('form-group'); + $password->setLabel('Password (leave blank if you don\'t want to change it)'); + $password->setRequired(false); + $form->addField($password); + + $submit = new Form\Element\Submit(); + $submit->setClass('btn btn-success'); + $submit->setValue('Save »'); + $form->addField($submit); + + $form->setValues($values); + + $this->view->form = $form; + + return $this->view->render(); + } + /** * Add a user - handles both form and processing. */ public function add() { if (!$_SESSION['user']->getIsAdmin()) { - throw new \Exception('You do not have permission to do that.'); + throw new ForbiddenException('You do not have permission to do that.'); } $this->config->set('page_title', 'Add User'); - $method = $this->request->getMethod(); if ($method == 'POST') { @@ -77,7 +133,6 @@ class UserController extends Controller } $values = $form->getValues(); - $values['is_admin'] = $values['admin'] ? 1 : 0; $values['hash'] = password_hash($values['password'], PASSWORD_DEFAULT); $user = new User(); @@ -95,42 +150,40 @@ class UserController extends Controller public function edit($userId) { if (!$_SESSION['user']->getIsAdmin()) { - throw new \Exception('You do not have permission to do that.'); + throw new ForbiddenException('You do not have permission to do that.'); } - $method = $this->request->getMethod(); - $user = $this->userStore->getById($userId); + $method = $this->request->getMethod(); + $user = $this->userStore->getById($userId); - $this->config->set('page_title', 'Edit: ' . $user->getName()); - - - if ($method == 'POST') { - $values = $this->getParams(); - } else { - $values = $user->getDataArray(); - $values['admin'] = $values['is_admin']; + if (empty($user)) { + throw new NotFoundException('User with ID: ' . $userId . ' does not exist.'); } - $form = $this->userForm($values, 'edit/' . $userId); + $values = array_merge($user->getDataArray(), $this->getParams()); + $form = $this->userForm($values, 'edit/' . $userId); if ($method != 'POST' || ($method == 'POST' && !$form->validate())) { - $view = new b8\View('UserForm'); - $view->type = 'edit'; - $view->user = $user; - $view->form = $form; + $view = new b8\View('UserForm'); + $view->type = 'edit'; + $view->user = $user; + $view->form = $form; return $view->render(); } - $values = $form->getValues(); - $values['is_admin'] = $values['admin'] ? 1 : 0; - if (!empty($values['password'])) { $values['hash'] = password_hash($values['password'], PASSWORD_DEFAULT); } $user->setValues($values); - $user = $this->userStore->save($user); + + $isAdmin = $this->getParam('is_admin'); + if (empty($isAdmin)) { + $user->setIsAdmin(0); + } + + $this->userStore->save($user); header('Location: '.PHPCI_URL.'user'); die; @@ -161,13 +214,20 @@ class UserController extends Controller $form->addField($field); $field = new Form\Element\Password('password'); - $field->setRequired(true); - $field->setLabel('Password' . ($type == 'edit' ? ' (leave blank to keep current password)' : '')); + + if ($type == 'add') { + $field->setRequired(true); + $field->setLabel('Password'); + } else { + $field->setRequired(false); + $field->setLabel('Password (leave blank to keep current password)'); + } + $field->setClass('form-control'); $field->setContainerClass('form-group'); $form->addField($field); - $field = new Form\Element\Checkbox('admin'); + $field = new Form\Element\Checkbox('is_admin'); $field->setRequired(false); $field->setCheckedValue(1); $field->setLabel('Is this user an administrator?'); @@ -189,10 +249,15 @@ class UserController extends Controller public function delete($userId) { if (!$_SESSION['user']->getIsAdmin()) { - throw new \Exception('You do not have permission to do that.'); + throw new ForbiddenException('You do not have permission to do that.'); } $user = $this->userStore->getById($userId); + + if (empty($user)) { + throw new NotFoundException('User with ID: ' . $userId . ' does not exist.'); + } + $this->userStore->delete($user); header('Location: '.PHPCI_URL.'user'); diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 9a9d143b..e95a5597 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -2,15 +2,16 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Controller; use b8; use b8\Store; +use PHPCI\BuildFactory; use PHPCI\Model\Build; /** @@ -42,21 +43,16 @@ class WebhookController extends \PHPCI\Controller foreach ($payload['commits'] as $commit) { try { + $email = $commit['raw_author']; $email = substr($email, 0, strpos($email, '>')); $email = substr($email, strpos($email, '<') + 1); - $build = new Build(); - $build->setProjectId($project); - $build->setCommitId($commit['raw_node']); - $build->setCommitterEmail($email); - $build->setStatus(Build::STATUS_NEW); - $build->setLog(''); - $build->setCreated(new \DateTime()); - $build->setBranch($commit['branch']); - $build->setCommitMessage($commit['message']); - $this->buildStore->save($build); + $this->createBuild($project, $commit['raw_node'], $commit['branch'], $email, $commit['message']); } catch (\Exception $ex) { + header('HTTP/1.1 500 Internal Server Error'); + header('Ex: ' . $ex->getMessage()); + die('FAIL'); } } @@ -74,36 +70,22 @@ class WebhookController extends \PHPCI\Controller $commit = $this->getParam('commit'); try { - $build = new Build(); - $build->setProjectId($project); - - if ($branch !== null && trim($branch) !== '') { - $build->setBranch($branch); - } else { - $build->setBranch('master'); + if (empty($branch)) { + $branch = 'master'; } - if ($commit !== null && trim($commit) !== '') { - $build->setCommitId($commit); + if (empty($commit)) { + $commit = null; } - $build->setStatus(Build::STATUS_NEW); - $build->setLog(''); - $build->setCreated(new \DateTime()); + $this->createBuild($project, $commit, $branch, null, null); + } catch (\Exception $ex) { header('HTTP/1.1 400 Bad Request'); header('Ex: ' . $ex->getMessage()); die('FAIL'); } - try { - $this->buildStore->save($build); /** bugfix: Errors with PHPCI GitHub hook #296 */ - } catch (\Exception $ex) { - header('HTTP/1.1 500 Internal Server Error'); - header('Ex: ' . $ex->getMessage()); - die('FAIL'); - } - die('OK'); } @@ -112,11 +94,27 @@ class WebhookController extends \PHPCI\Controller */ public function github($project) { - $payload = json_decode($this->getParam('payload'), true); + $payload = json_decode($this->getParam('payload'), true); + // Handle Pull Request web hooks: + if (array_key_exists('pull_request', $payload)) { + return $this->githubPullRequest($project, $payload); + } + + // Handle Push web hooks: + if (array_key_exists('commits', $payload)) { + return $this->githubCommitRequest($project, $payload); + } + + header('HTTP/1.1 200 OK'); + die('This request type is not supported, this is not an error.'); + } + + protected function githubCommitRequest($project, array $payload) + { // Github sends a payload when you close a pull request with a // non-existant commit. We don't want this. - if ($payload['after'] === '0000000000000000000000000000000000000000') { + if (array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') { die('OK'); } @@ -130,32 +128,16 @@ class WebhookController extends \PHPCI\Controller continue; } - $build = new Build(); - $build->setProjectId($project); - $build->setCommitId($commit['id']); - $build->setStatus(Build::STATUS_NEW); - $build->setLog(''); - $build->setCreated(new \DateTime()); - $build->setBranch(str_replace('refs/heads/', '', $payload['ref'])); - $build->setCommitterEmail($commit['committer']['email']); - $build->setCommitMessage($commit['message']); - $build = $this->buildStore->save($build); - $build->sendStatusPostback(); + $branch = str_replace('refs/heads/', '', $payload['ref']); + $committer = $commit['committer']['email']; + $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); } } elseif (substr($payload['ref'], 0, 10) == 'refs/tags/') { // If we don't, but we're dealing with a tag, add that instead: - $build = new Build(); - $build->setProjectId($project); - $build->setCommitId($payload['after']); - $build->setStatus(Build::STATUS_NEW); - $build->setLog(''); - $build->setCreated(new \DateTime()); - $build->setBranch(str_replace('refs/tags/', 'Tag: ', $payload['ref'])); - $build->setCommitterEmail($payload['pusher']['email']); - $build->setCommitMessage($payload['head_commit']['message']); - - $build = $this->buildStore->save($build); - $build->sendStatusPostback(); + $branch = str_replace('refs/tags/', 'Tag: ', $payload['ref']); + $committer = $payload['pusher']['email']; + $message = $payload['head_commit']['message']; + $this->createBuild($project, $payload['after'], $branch, $committer, $message); } } catch (\Exception $ex) { @@ -167,6 +149,57 @@ class WebhookController extends \PHPCI\Controller die('OK'); } + protected function githubPullRequest($projectId, array $payload) + { + // We only want to know about open pull requests: + if (!in_array($payload['action'], array('opened', 'synchronize', 'reopened'))) { + die('OK'); + } + + try { + $headers = array(); + $token = \b8\Config::getInstance()->get('phpci.github.token'); + + if (!empty($token)) { + $headers[] = 'Authorization: token ' . $token; + } + + $url = $payload['pull_request']['commits_url']; + $http = new \b8\HttpClient(); + $http->setHeaders($headers); + $response = $http->get($url); + + // Check we got a success response: + if (!$response['success']) { + header('HTTP/1.1 500 Internal Server Error'); + header('Ex: Could not get commits, failed API request.'); + die('FAIL'); + } + + foreach ($response['body'] as $commit) { + $branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']); + $committer = $commit['commit']['author']['email']; + $message = $commit['commit']['message']; + + $extra = array( + 'build_type' => 'pull_request', + 'pull_request_id' => $payload['pull_request']['id'], + 'pull_request_number' => $payload['number'], + 'remote_branch' => $payload['pull_request']['head']['ref'], + 'remote_url' => $payload['pull_request']['head']['repo']['clone_url'], + ); + + $this->createBuild($projectId, $commit['sha'], $branch, $committer, $message, $extra); + } + } catch (\Exception $ex) { + header('HTTP/1.1 500 Internal Server Error'); + header('Ex: ' . $ex->getMessage()); + die('FAIL'); + } + + die('OK'); + } + /** * Called by Gitlab Webhooks: */ @@ -181,17 +214,9 @@ class WebhookController extends \PHPCI\Controller // If we have a list of commits, then add them all as builds to be tested: foreach ($payload['commits'] as $commit) { - $build = new Build(); - $build->setProjectId($project); - $build->setCommitId($commit['id']); - $build->setStatus(Build::STATUS_NEW); - $build->setLog(''); - $build->setCreated(new \DateTime()); - $build->setBranch(str_replace('refs/heads/', '', $payload['ref'])); - $build->setCommitterEmail($commit['author']['email']); - $build->setCommitMessage($commit['message']); - $build = $this->buildStore->save($build); - $build->sendStatusPostback(); + $branch = str_replace('refs/heads/', '', $payload['ref']); + $committer = $commit['author']['email']; + $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); } } @@ -203,4 +228,36 @@ class WebhookController extends \PHPCI\Controller die('OK'); } + + protected function createBuild($projectId, $commitId, $branch, $committer, $commitMessage, $extra = null) + { + // Check if a build already exists for this commit ID: + $builds = $this->buildStore->getByProjectAndCommit($projectId, $commitId); + + if ($builds['count']) { + return true; + } + + // If not, create a new build job for it: + $build = new Build(); + $build->setProjectId($projectId); + $build->setCommitId($commitId); + $build->setStatus(Build::STATUS_NEW); + $build->setLog(''); + $build->setCreated(new \DateTime()); + $build->setBranch($branch); + $build->setCommitterEmail($committer); + $build->setCommitMessage($commitMessage); + + if (!is_null($extra)) { + $build->setExtra(json_encode($extra)); + } + + $build = BuildFactory::getBuild($this->buildStore->save($build)); + + // Send a status postback if the build type provides one: + $build->sendStatusPostback(); + + return true; + } } diff --git a/PHPCI/Helper/BaseCommandExecutor.php b/PHPCI/Helper/BaseCommandExecutor.php new file mode 100644 index 00000000..9f13dab6 --- /dev/null +++ b/PHPCI/Helper/BaseCommandExecutor.php @@ -0,0 +1,108 @@ +logger = $logger; + $this->quiet = $quiet; + $this->verbose = $verbose; + + $this->lastOutput = array(); + + $this->rootDir = $rootDir; + } + + /** + * Executes shell commands. + * @param array $args + * @return bool Indicates success + */ + public function executeCommand($args = array()) + { + $this->lastOutput = array(); + + $command = call_user_func_array('sprintf', $args); + + if ($this->quiet) { + $this->logger->log('Executing: ' . $command); + } + + $status = 0; + exec($command, $this->lastOutput, $status); + + foreach ($this->lastOutput as &$lastOutput) { + $lastOutput = trim($lastOutput, '"'); + } + + if ($this->logExecOutput && !empty($this->lastOutput) && ($this->verbose|| $status != 0)) { + $this->logger->log($this->lastOutput); + } + + $rtn = false; + + if ($status == 0) { + $rtn = true; + } + + return $rtn; + } + + /** + * Returns the output from the last command run. + */ + public function getLastOutput() + { + return implode(PHP_EOL, $this->lastOutput); + } + + /** + * Find a binary required by a plugin. + * @param string $binary + * @return null|string + */ + abstract public function findBinary($binary); +} diff --git a/PHPCI/Helper/Build.php b/PHPCI/Helper/Build.php index 3aa7403f..7e3d28d3 100644 --- a/PHPCI/Helper/Build.php +++ b/PHPCI/Helper/Build.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Helper; diff --git a/PHPCI/Helper/BuildInterpolator.php b/PHPCI/Helper/BuildInterpolator.php index fef51bee..90b78681 100644 --- a/PHPCI/Helper/BuildInterpolator.php +++ b/PHPCI/Helper/BuildInterpolator.php @@ -1,4 +1,11 @@ logger = $logger; - $this->quiet = $quiet; - $this->verbose = $verbose; - - $this->lastOutput = array(); - - $this->rootDir = $rootDir; - } - /** * Executes shell commands. Accepts multiple arguments the first * is the template and everything else is inserted in. c.f. sprintf * @return bool Indicates success */ - public function executeCommand() - { - return $this->buildAndExecuteCommand(func_get_args()); - } - - /** - * Executes shell commands. - * @param array $args - * @return bool Indicates success - */ - public function buildAndExecuteCommand($args = array()) - { - $this->lastOutput = array(); - - $command = call_user_func_array('sprintf', $args); - - if ($this->quiet) { - $this->logger->log('Executing: ' . $command); - } - - $status = 0; - exec($command, $this->lastOutput, $status); - - foreach ($this->lastOutput as &$lastOutput) { - $lastOutput = trim($lastOutput, '"'); - } - - if ($this->logExecOutput && !empty($this->lastOutput) && ($this->verbose|| $status != 0)) { - $this->logger->log($this->lastOutput); - } - - $rtn = false; - - if ($status == 0) { - $rtn = true; - } - - return $rtn; - } + public function executeCommand(); /** * Returns the output from the last command run. */ - public function getLastOutput() - { - return implode(PHP_EOL, $this->lastOutput); - } + public function getLastOutput(); /** * Find a binary required by a plugin. - * @param $binary + * @param string $binary * @return null|string */ - public function findBinary($binary) - { - if (is_string($binary)) { - $binary = array($binary); - } - - foreach ($binary as $bin) { - $this->logger->log("Looking for binary: " . $bin, LogLevel::DEBUG); - // Check project root directory: - if (is_file($this->rootDir . $bin)) { - $this->logger->log("Found in root: " . $bin, LogLevel::DEBUG); - return $this->rootDir . $bin; - } - - // Check Composer bin dir: - if (is_file($this->rootDir . 'vendor/bin/' . $bin)) { - $this->logger->log("Found in vendor/bin: " . $bin, LogLevel::DEBUG); - return $this->rootDir . 'vendor/bin/' . $bin; - } - - // Use "where" for windows and "which" for other OS - $findCmd = IS_WIN ? 'where' : 'which'; - $findCmdResult = trim(shell_exec($findCmd . ' ' . $bin)); - - if (!empty($findCmdResult)) { - $this->logger->log("Found in " . $findCmdResult, LogLevel::DEBUG); - return $findCmdResult; - } - } - - return null; - } + public function findBinary($binary); } diff --git a/PHPCI/Helper/Email.php b/PHPCI/Helper/Email.php new file mode 100644 index 00000000..7c44e0f7 --- /dev/null +++ b/PHPCI/Helper/Email.php @@ -0,0 +1,134 @@ +'; + + protected $emailTo = array(); + protected $emailCc = array(); + protected $subject = 'Email from PHPCI'; + protected $body = ''; + protected $isHtml = false; + protected $config; + + public function __construct() + { + $this->config = Config::getInstance(); + } + + public function setEmailTo($email, $name = null) + { + $this->emailTo[$email] = $name; + + return $this; + } + + public function addCc($email, $name = null) + { + $this->emailCc[$email] = $name; + + return $this; + } + + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + public function setBody($body) + { + $this->body = $body; + + return $this; + } + + public function setIsHtml($isHtml = false) + { + $this->isHtml = $isHtml; + + return $this; + } + + public function send() + { + $smtpServer = $this->config->get('phpci.email_settings.smtp_address'); + + if (empty($smtpServer)) { + return $this->sendViaMail(); + } else { + return $this->sendViaSwiftMailer(); + } + } + + protected function sendViaMail() + { + $headers = ''; + + if ($this->isHtml) { + $headers = 'Content-Type: text/html' . PHP_EOL; + } + + $headers .= 'From: ' . $this->getFrom() . PHP_EOL; + + $emailTo = array(); + foreach ($this->emailTo as $email => $name) { + $thisTo = $email; + + if (!is_null($name)) { + $thisTo = '"' . $name . '" <' . $thisTo . '>'; + } + + $emailTo[] = $thisTo; + } + + $emailTo = implode(', ', $emailTo); + + return mail($emailTo, $this->subject, $this->body, $headers); + } + + protected function sendViaSwiftMailer() + { + $factory = new MailerFactory($this->config->get('phpci')); + $mailer = $factory->getSwiftMailerFromConfig(); + + $message = \Swift_Message::newInstance($this->subject) + ->setFrom($this->getFrom()) + ->setTo($this->emailTo) + ->setBody($this->body); + + if ($this->isHtml) { + $message->setContentType('text/html'); + } + + if (is_array($this->emailCc) && count($this->emailCc)) { + $message->setCc($this->emailCc); + } + + return $mailer->send($message); + } + + protected function getFrom() + { + $email = $this->config->get('phpci.email_settings.from_address', self::DEFAULT_FROM); + + if (empty($email)) { + $email = self::DEFAULT_FROM; + } + + return $email; + } +} diff --git a/PHPCI/Helper/Github.php b/PHPCI/Helper/Github.php index 6ce2d556..24d419d3 100644 --- a/PHPCI/Helper/Github.php +++ b/PHPCI/Helper/Github.php @@ -1,4 +1,11 @@ logger->log("Looking for binary: " . $bin, LogLevel::DEBUG); + + if (is_file($this->rootDir . $bin)) { + $this->logger->log("Found in root: " . $bin, LogLevel::DEBUG); + $binaryPath = $this->rootDir . $bin; + break; + } + + if (is_file($this->rootDir . 'vendor/bin/' . $bin)) { + $this->logger->log("Found in vendor/bin: " . $bin, LogLevel::DEBUG); + $binaryPath = $this->rootDir . 'vendor/bin/' . $bin; + break; + } + + $findCmdResult = trim(shell_exec('which ' . $bin)); + if (!empty($findCmdResult)) { + $this->logger->log("Found in " . $findCmdResult, LogLevel::DEBUG); + $binaryPath = $findCmdResult; + break; + } + } + return $binaryPath; + } +} diff --git a/PHPCI/Helper/User.php b/PHPCI/Helper/User.php index 64872992..726003fd 100644 --- a/PHPCI/Helper/User.php +++ b/PHPCI/Helper/User.php @@ -1,11 +1,11 @@ logger->log("Looking for binary: " . $bin, LogLevel::DEBUG); + + if (is_file($this->rootDir . $bin)) { + $this->logger->log("Found in root: " . $bin, LogLevel::DEBUG); + $binaryPath = $this->rootDir . $bin; + break; + } + + if (is_file($this->rootDir . 'vendor/bin/' . $bin)) { + $this->logger->log("Found in vendor/bin: " . $bin, LogLevel::DEBUG); + $binaryPath = $this->rootDir . 'vendor/bin/' . $bin; + break; + } + + $findCmdResult = trim(shell_exec('where ' . $bin)); + if (!empty($findCmdResult)) { + $this->logger->log("Found in " . $findCmdResult, LogLevel::DEBUG); + $binaryPath = $findCmdResult; + break; + } + } + return $binaryPath; + } +} diff --git a/PHPCI/Logging/BuildDBLogHandler.php b/PHPCI/Logging/BuildDBLogHandler.php index a877fe50..3c03c501 100644 --- a/PHPCI/Logging/BuildDBLogHandler.php +++ b/PHPCI/Logging/BuildDBLogHandler.php @@ -1,8 +1,14 @@ output = $output; } - protected function write(array $record) { $this->output->writeln((string)$record['formatted']); diff --git a/PHPCI/Migrations/20140513143726_initial_migration.php b/PHPCI/Migrations/20140513143726_initial_migration.php new file mode 100644 index 00000000..8aad60b6 --- /dev/null +++ b/PHPCI/Migrations/20140513143726_initial_migration.php @@ -0,0 +1,218 @@ +createBuildTable(); + $this->createBuildMetaTable(); + $this->createProjectTable(); + $this->createUserTable(); + + // Set up foreign keys: + $build = $this->table('build'); + + if (!$build->hasForeignKey('project_id')) { + $build->addForeignKey('project_id', 'project', 'id', array('delete'=> 'CASCADE', 'update' => 'CASCADE')); + } + + $build->save(); + + $buildMeta = $this->table('build_meta'); + + if (!$buildMeta->hasForeignKey('build_id')) { + $buildMeta->addForeignKey('build_id', 'build', 'id', array('delete'=> 'CASCADE', 'update' => 'CASCADE')); + } + + if (!$buildMeta->hasForeignKey('project_id')) { + $buildMeta->addForeignKey('project_id', 'project', 'id', array('delete'=> 'CASCADE', 'update' => 'CASCADE')); + } + + $buildMeta->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + + protected function createBuildTable() + { + $table = $this->table('build'); + + if (!$table->hasColumn('project_id')) { + $table->addColumn('project_id', 'integer'); + } + + if (!$table->hasColumn('commit_id')) { + $table->addColumn('commit_id', 'string', array('limit' => 50)); + } + + if (!$table->hasColumn('status')) { + $table->addColumn('status', 'integer', array('limit' => 4)); + } + + if (!$table->hasColumn('log')) { + $table->addColumn('log', 'text'); + } + + if (!$table->hasColumn('branch')) { + $table->addColumn('branch', 'string', array('limit' => 50)); + } + + if (!$table->hasColumn('created')) { + $table->addColumn('created', 'datetime'); + } + + if (!$table->hasColumn('started')) { + $table->addColumn('started', 'datetime'); + } + + if (!$table->hasColumn('finished')) { + $table->addColumn('finished', 'datetime'); + } + + if (!$table->hasColumn('committer_email')) { + $table->addColumn('committer_email', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('commit_message')) { + $table->addColumn('commit_message', 'text'); + } + + if (!$table->hasColumn('extra')) { + $table->addColumn('extra', 'text'); + } + + if ($table->hasColumn('plugins')) { + $table->removeColumn('plugins'); + } + + if (!$table->hasIndex(array('project_id'))) { + $table->addIndex(array('project_id')); + } + + if (!$table->hasIndex(array('status'))) { + $table->addIndex(array('status')); + } + + $table->save(); + } + + protected function createBuildMetaTable() + { + $table = $this->table('build_meta'); + + if (!$table->hasColumn('project_id')) { + $table->addColumn('project_id', 'integer'); + } + + if (!$table->hasColumn('build_id')) { + $table->addColumn('build_id', 'integer'); + } + + if (!$table->hasColumn('meta_key')) { + $table->addColumn('meta_key', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('meta_value')) { + $table->addColumn('meta_value', 'text'); + } + + if (!$table->hasIndex(array('build_id', 'meta_key'))) { + $table->addIndex(array('build_id', 'meta_key')); + } + + $table->save(); + } + + protected function createProjectTable() + { + $table = $this->table('project'); + + if (!$table->hasColumn('title')) { + $table->addColumn('title', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('reference')) { + $table->addColumn('reference', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('git_key')) { + $table->addColumn('git_key', 'text'); + } + + if (!$table->hasColumn('public_key')) { + $table->addColumn('public_key', 'text'); + } + + if (!$table->hasColumn('type')) { + $table->addColumn('type', 'string', array('limit' => 50)); + } + + if (!$table->hasColumn('access_information')) { + $table->addColumn('access_information', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('last_commit')) { + $table->addColumn('last_commit', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('build_config')) { + $table->addColumn('build_config', 'text'); + } + + if (!$table->hasColumn('allow_public_status')) { + $table->addColumn('allow_public_status', 'integer'); + } + + if ($table->hasColumn('token')) { + $table->removeColumn('token'); + } + + if (!$table->hasIndex(array('title'))) { + $table->addIndex(array('title')); + } + + $table->save(); + } + + protected function createUserTable() + { + $table = $this->table('user'); + + if (!$table->hasColumn('email')) { + $table->addColumn('email', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('hash')) { + $table->addColumn('hash', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('name')) { + $table->addColumn('name', 'string', array('limit' => 250)); + } + + if (!$table->hasColumn('is_admin')) { + $table->addColumn('is_admin', 'integer'); + } + + if (!$table->hasIndex(array('email'))) { + $table->addIndex(array('email')); + } + + $table->save(); + } +} \ No newline at end of file diff --git a/PHPCI/Migrations/20140513153133_change_build_keys_migration.php b/PHPCI/Migrations/20140513153133_change_build_keys_migration.php new file mode 100644 index 00000000..b26a9388 --- /dev/null +++ b/PHPCI/Migrations/20140513153133_change_build_keys_migration.php @@ -0,0 +1,26 @@ +table('project'); + $project->renameColumn('git_key', 'ssh_private_key'); + $project->renameColumn('public_key', 'ssh_public_key'); + } + + /** + * Migrate Down. + */ + public function down() + { + $project = $this->table('project'); + $project->renameColumn('ssh_private_key', 'git_key'); + $project->renameColumn('ssh_public_key', 'public_key'); + } +} \ No newline at end of file diff --git a/PHPCI/Model.php b/PHPCI/Model.php index 7e314693..d1adf9ec 100644 --- a/PHPCI/Model.php +++ b/PHPCI/Model.php @@ -1,4 +1,12 @@ null, 'started' => null, 'finished' => null, - 'plugins' => null, 'committer_email' => null, 'commit_message' => null, + 'extra' => null, ); /** @@ -61,9 +61,9 @@ class BuildBase extends Model 'created' => 'getCreated', 'started' => 'getStarted', 'finished' => 'getFinished', - 'plugins' => 'getPlugins', 'committer_email' => 'getCommitterEmail', 'commit_message' => 'getCommitMessage', + 'extra' => 'getExtra', // Foreign key getters: 'Project' => 'getProject', @@ -83,9 +83,9 @@ class BuildBase extends Model 'created' => 'setCreated', 'started' => 'setStarted', 'finished' => 'setFinished', - 'plugins' => 'setPlugins', 'committer_email' => 'setCommitterEmail', 'commit_message' => 'setCommitMessage', + 'extra' => 'setExtra', // Foreign key setters: 'Project' => 'setProject', @@ -143,11 +143,6 @@ class BuildBase extends Model 'nullable' => true, 'default' => null, ), - 'plugins' => array( - 'type' => 'text', - 'nullable' => true, - 'default' => null, - ), 'committer_email' => array( 'type' => 'varchar', 'length' => 512, @@ -159,6 +154,11 @@ class BuildBase extends Model 'nullable' => true, 'default' => null, ), + 'extra' => array( + 'type' => 'longtext', + 'nullable' => true, + 'default' => null, + ), ); /** @@ -303,18 +303,6 @@ class BuildBase extends Model return $rtn; } - /** - * Get the value of Plugins / plugins. - * - * @return string - */ - public function getPlugins() - { - $rtn = $this->data['plugins']; - - return $rtn; - } - /** * Get the value of CommitterEmail / committer_email. * @@ -339,6 +327,18 @@ class BuildBase extends Model return $rtn; } + /** + * Get the value of Extra / extra. + * + * @return string + */ + public function getExtra() + { + $rtn = $this->data['extra']; + + return $rtn; + } + /** * Set the value of Id / id. * @@ -509,24 +509,6 @@ class BuildBase extends Model $this->_setModified('finished'); } - /** - * Set the value of Plugins / plugins. - * - * @param $value string - */ - public function setPlugins($value) - { - $this->_validateString('Plugins', $value); - - if ($this->data['plugins'] === $value) { - return; - } - - $this->data['plugins'] = $value; - - $this->_setModified('plugins'); - } - /** * Set the value of CommitterEmail / committer_email. * @@ -563,6 +545,24 @@ class BuildBase extends Model $this->_setModified('commit_message'); } + /** + * Set the value of Extra / extra. + * + * @param $value string + */ + public function setExtra($value) + { + $this->_validateString('Extra', $value); + + if ($this->data['extra'] === $value) { + return; + } + + $this->data['extra'] = $value; + + $this->_setModified('extra'); + } + /** * Get the Project model for this Build by Id. * diff --git a/PHPCI/Model/Base/BuildMetaBase.php b/PHPCI/Model/Base/BuildMetaBase.php index b7f85490..a979e1d9 100644 --- a/PHPCI/Model/Base/BuildMetaBase.php +++ b/PHPCI/Model/Base/BuildMetaBase.php @@ -52,6 +52,7 @@ class BuildMetaBase extends Model 'meta_value' => 'getMetaValue', // Foreign key getters: + 'Project' => 'getProject', 'Build' => 'getBuild', ); @@ -67,6 +68,7 @@ class BuildMetaBase extends Model 'meta_value' => 'setMetaValue', // Foreign key setters: + 'Project' => 'setProject', 'Build' => 'setBuild', ); @@ -98,7 +100,7 @@ class BuildMetaBase extends Model 'default' => null, ), 'meta_value' => array( - 'type' => 'text', + 'type' => 'longtext', 'nullable' => true, 'default' => null, ), @@ -110,12 +112,20 @@ class BuildMetaBase extends Model public $indexes = array( 'PRIMARY' => array('unique' => true, 'columns' => 'id'), 'idx_meta_id' => array('unique' => true, 'columns' => 'build_id, meta_key'), + 'project_id' => array('columns' => 'project_id'), ); /** * @var array */ public $foreignKeys = array( + 'build_meta_ibfk_1' => array( + 'local_col' => 'project_id', + 'update' => 'CASCADE', + 'delete' => 'CASCADE', + 'table' => 'project', + 'col' => 'id' + ), 'fk_meta_build_id' => array( 'local_col' => 'build_id', 'update' => 'CASCADE', @@ -281,6 +291,63 @@ class BuildMetaBase extends Model $this->_setModified('meta_value'); } + /** + * Get the Project model for this BuildMeta by Id. + * + * @uses \PHPCI\Store\ProjectStore::getById() + * @uses \PHPCI\Model\Project + * @return \PHPCI\Model\Project + */ + public function getProject() + { + $key = $this->getProjectId(); + + if (empty($key)) { + return null; + } + + $cacheKey = 'Cache.Project.' . $key; + $rtn = $this->cache->get($cacheKey, null); + + if (empty($rtn)) { + $rtn = Factory::getStore('Project', 'PHPCI')->getById($key); + $this->cache->set($cacheKey, $rtn); + } + + return $rtn; + } + + /** + * Set Project - Accepts an ID, an array representing a Project or a Project model. + * + * @param $value mixed + */ + public function setProject($value) + { + // Is this an instance of Project? + if ($value instanceof \PHPCI\Model\Project) { + return $this->setProjectObject($value); + } + + // Is this an array representing a Project item? + if (is_array($value) && !empty($value['id'])) { + return $this->setProjectId($value['id']); + } + + // Is this a scalar value representing the ID of this foreign key? + return $this->setProjectId($value); + } + + /** + * Set Project - Accepts a Project model. + * + * @param $value \PHPCI\Model\Project + */ + public function setProjectObject(\PHPCI\Model\Project $value) + { + return $this->setProjectId($value->getId()); + } + /** * Get the Build model for this BuildMeta by Id. * diff --git a/PHPCI/Model/Base/ProjectBase.php b/PHPCI/Model/Base/ProjectBase.php index 9e2f5922..00bee23a 100644 --- a/PHPCI/Model/Base/ProjectBase.php +++ b/PHPCI/Model/Base/ProjectBase.php @@ -36,10 +36,9 @@ class ProjectBase extends Model 'id' => null, 'title' => null, 'reference' => null, - 'git_key' => null, - 'public_key' => null, + 'ssh_private_key' => null, + 'ssh_public_key' => null, 'type' => null, - 'token' => null, 'access_information' => null, 'last_commit' => null, 'build_config' => null, @@ -54,10 +53,9 @@ class ProjectBase extends Model 'id' => 'getId', 'title' => 'getTitle', 'reference' => 'getReference', - 'git_key' => 'getGitKey', - 'public_key' => 'getPublicKey', + 'ssh_private_key' => 'getSshPrivateKey', + 'ssh_public_key' => 'getSshPublicKey', 'type' => 'getType', - 'token' => 'getToken', 'access_information' => 'getAccessInformation', 'last_commit' => 'getLastCommit', 'build_config' => 'getBuildConfig', @@ -74,10 +72,9 @@ class ProjectBase extends Model 'id' => 'setId', 'title' => 'setTitle', 'reference' => 'setReference', - 'git_key' => 'setGitKey', - 'public_key' => 'setPublicKey', + 'ssh_private_key' => 'setSshPrivateKey', + 'ssh_public_key' => 'setSshPublicKey', 'type' => 'setType', - 'token' => 'setToken', 'access_information' => 'setAccessInformation', 'last_commit' => 'setLastCommit', 'build_config' => 'setBuildConfig', @@ -107,12 +104,12 @@ class ProjectBase extends Model 'length' => 250, 'default' => null, ), - 'git_key' => array( + 'ssh_private_key' => array( 'type' => 'text', 'nullable' => true, 'default' => null, ), - 'public_key' => array( + 'ssh_public_key' => array( 'type' => 'text', 'nullable' => true, 'default' => null, @@ -122,12 +119,6 @@ class ProjectBase extends Model 'length' => 50, 'default' => 1, ), - 'token' => array( - 'type' => 'varchar', - 'length' => 50, - 'nullable' => true, - 'default' => null, - ), 'access_information' => array( 'type' => 'varchar', 'length' => 250, @@ -202,25 +193,25 @@ class ProjectBase extends Model } /** - * Get the value of GitKey / git_key. + * Get the value of SshPrivateKey / ssh_private_key. * * @return string */ - public function getGitKey() + public function getSshPrivateKey() { - $rtn = $this->data['git_key']; + $rtn = $this->data['ssh_private_key']; return $rtn; } /** - * Get the value of PublicKey / public_key. + * Get the value of SshPublicKey / ssh_public_key. * * @return string */ - public function getPublicKey() + public function getSshPublicKey() { - $rtn = $this->data['public_key']; + $rtn = $this->data['ssh_public_key']; return $rtn; } @@ -237,18 +228,6 @@ class ProjectBase extends Model return $rtn; } - /** - * Get the value of Token / token. - * - * @return string - */ - public function getToken() - { - $rtn = $this->data['token']; - - return $rtn; - } - /** * Get the value of AccessInformation / access_information. * @@ -358,39 +337,39 @@ class ProjectBase extends Model } /** - * Set the value of GitKey / git_key. + * Set the value of SshPrivateKey / ssh_private_key. * * @param $value string */ - public function setGitKey($value) + public function setSshPrivateKey($value) { - $this->_validateString('GitKey', $value); + $this->_validateString('SshPrivateKey', $value); - if ($this->data['git_key'] === $value) { + if ($this->data['ssh_private_key'] === $value) { return; } - $this->data['git_key'] = $value; + $this->data['ssh_private_key'] = $value; - $this->_setModified('git_key'); + $this->_setModified('ssh_private_key'); } /** - * Set the value of PublicKey / public_key. + * Set the value of SshPublicKey / ssh_public_key. * * @param $value string */ - public function setPublicKey($value) + public function setSshPublicKey($value) { - $this->_validateString('PublicKey', $value); + $this->_validateString('SshPublicKey', $value); - if ($this->data['public_key'] === $value) { + if ($this->data['ssh_public_key'] === $value) { return; } - $this->data['public_key'] = $value; + $this->data['ssh_public_key'] = $value; - $this->_setModified('public_key'); + $this->_setModified('ssh_public_key'); } /** @@ -413,24 +392,6 @@ class ProjectBase extends Model $this->_setModified('type'); } - /** - * Set the value of Token / token. - * - * @param $value string - */ - public function setToken($value) - { - $this->_validateString('Token', $value); - - if ($this->data['token'] === $value) { - return; - } - - $this->data['token'] = $value; - - $this->_setModified('token'); - } - /** * Set the value of AccessInformation / access_information. * @@ -516,4 +477,16 @@ class ProjectBase extends Model { return Factory::getStore('Build', 'PHPCI')->getByProjectId($this->getId()); } + + /** + * Get BuildMeta models by ProjectId for this Project. + * + * @uses \PHPCI\Store\BuildMetaStore::getByProjectId() + * @uses \PHPCI\Model\BuildMeta + * @return \PHPCI\Model\BuildMeta[] + */ + public function getProjectBuildMetas() + { + return Factory::getStore('BuildMeta', 'PHPCI')->getByProjectId($this->getId()); + } } diff --git a/PHPCI/Model/Build.php b/PHPCI/Model/Build.php index e8b8cd5a..4ca3cf9e 100644 --- a/PHPCI/Model/Build.php +++ b/PHPCI/Model/Build.php @@ -1,11 +1,11 @@ array( 'ignore' => array( - 'vendor/', + 'vendor', ) ) ); @@ -164,4 +164,19 @@ class Build extends BuildBase { return null; } + + public function getExtra($key = null) + { + $data = json_decode($this->data['extra'], true); + + if (is_null($key)) { + $rtn = $data; + } elseif (isset($data[$key])) { + $rtn = $data[$key]; + } else { + $rtn = null; + } + + return $rtn; + } } diff --git a/PHPCI/Model/Build/BitbucketBuild.php b/PHPCI/Model/Build/BitbucketBuild.php index fb50a710..3af0d2b5 100644 --- a/PHPCI/Model/Build/BitbucketBuild.php +++ b/PHPCI/Model/Build/BitbucketBuild.php @@ -1,11 +1,11 @@ getProject()->getGitKey()); + $key = trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { return 'git@bitbucket.org:' . $this->getProject()->getReference() . '.git'; diff --git a/PHPCI/Model/Build/GithubBuild.php b/PHPCI/Model/Build/GithubBuild.php index f358355a..6d3f217b 100644 --- a/PHPCI/Model/Build/GithubBuild.php +++ b/PHPCI/Model/Build/GithubBuild.php @@ -1,14 +1,15 @@ getProject()->getGitKey()); + $key = trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { return 'git@github.com:' . $this->getProject()->getReference() . '.git'; @@ -115,4 +116,29 @@ class GithubBuild extends RemoteGitBuild return $link; } + + protected function postCloneSetup(Builder $builder, $cloneTo) + { + $buildType = $this->getExtra('build_type'); + + $success = true; + + try { + if (!empty($buildType) && $buildType == 'pull_request') { + $remoteUrl = $this->getExtra('remote_url'); + $remoteBranch = $this->getExtra('remote_branch'); + + $cmd = 'cd "%s" && git checkout -b phpci/' . $this->getId() . ' %s && git pull -q --no-edit %s %s'; + $success = $builder->executeCommand($cmd, $cloneTo, $this->getBranch(), $remoteUrl, $remoteBranch); + } + } catch (\Exception $ex) { + $success = false; + } + + if ($success) { + $success = parent::postCloneSetup($builder, $cloneTo); + } + + return $success; + } } diff --git a/PHPCI/Model/Build/GitlabBuild.php b/PHPCI/Model/Build/GitlabBuild.php index 1ce777ae..83053090 100644 --- a/PHPCI/Model/Build/GitlabBuild.php +++ b/PHPCI/Model/Build/GitlabBuild.php @@ -1,11 +1,11 @@ getProject()->getGitKey()); + $key = trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { $user = $this->getProject()->getAccessInformation("user"); diff --git a/PHPCI/Model/Build/LocalBuild.php b/PHPCI/Model/Build/LocalBuild.php index 9f2be613..3fd5e532 100644 --- a/PHPCI/Model/Build/LocalBuild.php +++ b/PHPCI/Model/Build/LocalBuild.php @@ -1,11 +1,11 @@ getProject()->getGitKey()); + $key = trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { $success = $this->cloneBySsh($builder, $buildPath); @@ -65,12 +65,8 @@ class RemoteGitBuild extends Build $cmd .= ' -b %s %s "%s"'; $success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo); - if (!empty($commit) && $commit != 'Manual') { - $cmd = 'cd "%s" && git checkout %s'; - if (IS_WIN) { - $cmd = 'cd /d "%s" && git checkout %s'; - } - $builder->executeCommand($cmd, $cloneTo, $this->getCommitId()); + if ($success) { + $success = $this->postCloneSetup($builder, $cloneTo); } return $success; @@ -104,15 +100,8 @@ class RemoteGitBuild extends Build $success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo); - // Checkout a specific commit if we need to: - $commit = $this->getCommitId(); - - if (!empty($commit) && $commit != 'Manual') { - $cmd = 'cd "%s" && git checkout %s'; - if (IS_WIN) { - $cmd = 'cd /d "%s" && git checkout %s'; - } - $builder->executeCommand($cmd, $cloneTo, $this->getCommitId()); + if ($success) { + $success = $this->postCloneSetup($builder, $cloneTo); } // Remove the key file and git wrapper: @@ -122,6 +111,24 @@ class RemoteGitBuild extends Build return $success; } + protected function postCloneSetup(Builder $builder, $cloneTo) + { + $success = true; + $commit = $this->getCommitId(); + + if (!empty($commit) && $commit != 'Manual') { + $cmd = 'cd "%s" && git checkout %s'; + + if (IS_WIN) { + $cmd = 'cd /d "%s" && git checkout %s'; + } + + $success = $builder->executeCommand($cmd, $cloneTo, $this->getCommitId()); + } + + return $success; + } + /** * Create an SSH key file on disk for this build. * @param $cloneTo @@ -133,7 +140,7 @@ class RemoteGitBuild extends Build $keyFile = $keyPath . '.key'; // Write the contents of this project's git key to the file: - file_put_contents($keyFile, $this->getProject()->getGitKey()); + file_put_contents($keyFile, $this->getProject()->getSshPrivateKey()); chmod($keyFile, 0600); // Return the filename: diff --git a/PHPCI/Model/BuildMeta.php b/PHPCI/Model/BuildMeta.php index caa4b2fa..fc2af129 100644 --- a/PHPCI/Model/BuildMeta.php +++ b/PHPCI/Model/BuildMeta.php @@ -1,7 +1,10 @@ phpci->executeCommand($cmd, $this->phpci->buildPath . 'composer.phar'); $this->phpci->executeCommand($cmd, $this->phpci->buildPath . 'composer.lock'); diff --git a/PHPCI/Plugin/Codeception.php b/PHPCI/Plugin/Codeception.php index 5b811896..e29b81a2 100644 --- a/PHPCI/Plugin/Codeception.php +++ b/PHPCI/Plugin/Codeception.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Plugin; diff --git a/PHPCI/Plugin/Composer.php b/PHPCI/Plugin/Composer.php index 3a13b672..dbea469c 100644 --- a/PHPCI/Plugin/Composer.php +++ b/PHPCI/Plugin/Composer.php @@ -1,11 +1,11 @@ buildPath; - $this->phpci = $phpci; + $path = $phpci->buildPath; + $this->phpci = $phpci; $this->build = $build; - $this->directory = isset($options['directory']) ? $path . '/' . $options['directory'] : $path; - $this->action = isset($options['action']) ? $options['action'] : 'install'; - $this->preferDist = isset($options['prefer_dist']) ? $options['prefer_dist'] : true; + $this->directory = $path; + $this->action = 'install'; + $this->preferDist = false; + + if (array_key_exists('directory', $options)) { + $this->directory = $path . '/' . $options['directory']; + } + + if (array_key_exists('action', $options)) { + $this->action = $options['action']; + } + + if (array_key_exists('prefer_dist', $options)) { + $this->preferDist = (bool)$options['prefer_dist']; + } } /** @@ -59,12 +71,24 @@ class Composer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $this->phpci->logFailure('Could not find Composer.'); return false; } + $cmd = ''; + if (IS_WIN) { $cmd = 'php '; } + $cmd .= $composerLocation . ' --no-ansi --no-interaction '; - $cmd .= ($this->preferDist ? '--prefer-dist' : null) . ' --working-dir="%s" %s'; + + if ($this->preferDist) { + $this->phpci->log('Using --prefer-dist flag'); + $cmd .= '--prefer-dist'; + } else { + $this->phpci->log('Using --prefer-source flag'); + $cmd .= '--prefer-source'; + } + + $cmd .= ' --working-dir="%s" %s'; return $this->phpci->executeCommand($cmd, $this->directory, $this->action); } diff --git a/PHPCI/Plugin/CopyBuild.php b/PHPCI/Plugin/CopyBuild.php index 021d5e80..ca7dae76 100644 --- a/PHPCI/Plugin/CopyBuild.php +++ b/PHPCI/Plugin/CopyBuild.php @@ -1,11 +1,11 @@ phpci->executeCommand($cmd, $this->directory, $build, $this->directory); if ($this->ignore) { foreach ($this->phpci->ignore as $file) { $cmd = 'rm -Rf "%s/%s"'; + if (IS_WIN) { + $cmd = 'rmdir /S /Q "%s\%s"'; + } $this->phpci->executeCommand($cmd, $this->directory, $file); } } diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 0b23a1d7..70fca197 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -1,11 +1,11 @@ build->getBranch(); - return $this->phpci->executeCommand($cmd, $this->directory, $options['branch']); + $cmd = 'cd "%s" && git checkout %s && git merge "%s"'; + $path = $this->phpci->buildPath; + return $this->phpci->executeCommand($cmd, $path, $options['branch'], $this->build->getBranch()); } } diff --git a/PHPCI/Plugin/Grunt.php b/PHPCI/Plugin/Grunt.php index 0c845184..106a11b4 100644 --- a/PHPCI/Plugin/Grunt.php +++ b/PHPCI/Plugin/Grunt.php @@ -1,11 +1,11 @@ + * @package PHPCI + * @subpackage Plugins + */ +class HipchatNotify implements \PHPCI\Plugin +{ + private $authToken; + private $userAgent; + private $cookie; + + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $this->phpci = $phpci; + $this->build = $build; + + $this->userAgent = "PHPCI/1.0 (+http://www.phptesting.org/)"; + $this->cookie = "phpcicookie"; + + if (is_array($options) && isset($options['authToken']) && isset($options['room'])) { + $this->authToken = $options['authToken']; + $this->room = $options['room']; + + if (isset($options['message'])) { + $this->message = $options['message']; + } else { + $this->message = '%PROJECT_TITLE% built at %BUILD_URI%'; + } + } else { + throw new \Exception('Please define room and authToken for hipchat_notify plugin!'); + } + + } + + public function execute() + { + $hipChat = new \HipChat\HipChat($this->authToken); + $message = $this->phpci->interpolate($this->message); + + if (is_array($this->room)) { + foreach ($this->room as $room) { + $hipChat->message_room($room, 'PHPCI', $message); + } + } else { + $hipChat->message_room($this->room, 'PHPCI', $message); + } + } +} diff --git a/PHPCI/Plugin/Irc.php b/PHPCI/Plugin/Irc.php index 0defc037..fd774b8c 100644 --- a/PHPCI/Plugin/Irc.php +++ b/PHPCI/Plugin/Irc.php @@ -1,4 +1,11 @@ phpci = $phpci; @@ -74,19 +91,18 @@ class Mysql implements \PHPCI\Plugin /** * Connects to MySQL and runs a specified set of queries. + * @return boolean */ public function execute() { - $success = true; - try { $opts = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); - $this->pdo = new PDO('mysql:host=' . $this->host, $this->user, $this->pass, $opts); + $pdo = new PDO('mysql:host=' . $this->host, $this->user, $this->pass, $opts); foreach ($this->queries as $query) { if (!is_array($query)) { // Simple query - $this->pdo->query($this->phpci->interpolate($query)); + $pdo->query($this->phpci->interpolate($query)); } elseif (isset($query['import'])) { // SQL file execution $this->executeFile($query['import']); @@ -98,10 +114,14 @@ class Mysql implements \PHPCI\Plugin $this->phpci->logFailure($ex->getMessage()); return false; } - - return $success; + return true; } + /** + * @param string $query + * @return boolean + * @throws \Exception + */ protected function executeFile($query) { if (!isset($query['file'])) { diff --git a/PHPCI/Plugin/PackageBuild.php b/PHPCI/Plugin/PackageBuild.php index 3036d13b..e5dcda41 100644 --- a/PHPCI/Plugin/PackageBuild.php +++ b/PHPCI/Plugin/PackageBuild.php @@ -1,11 +1,11 @@ phpci = $phpci; - $this->build = $build; + $this->phpci = $phpci; + $this->build = $build; $this->queries = $options; $buildSettings = $phpci->getConfig('build_settings'); @@ -47,6 +74,7 @@ class Pgsql implements \PHPCI\Plugin /** * Connects to PgSQL and runs a specified set of queries. + * @return boolean */ public function execute() { @@ -61,7 +89,6 @@ class Pgsql implements \PHPCI\Plugin $this->phpci->logFailure($ex->getMessage()); return false; } - return true; } } diff --git a/PHPCI/Plugin/Phing.php b/PHPCI/Plugin/Phing.php index 270b3aa9..21f7a972 100644 --- a/PHPCI/Plugin/Phing.php +++ b/PHPCI/Plugin/Phing.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Plugin; diff --git a/PHPCI/Plugin/PhpCodeSniffer.php b/PHPCI/Plugin/PhpCodeSniffer.php index 7c7f81e0..3ecf2f48 100755 --- a/PHPCI/Plugin/PhpCodeSniffer.php +++ b/PHPCI/Plugin/PhpCodeSniffer.php @@ -1,11 +1,11 @@ suffixes = (array)$options['suffixes']; } - if (isset($options['directory'])) { - $this->directory = $options['directory']; - } - - if (isset($options['standard'])) { - $this->standard = $options['standard']; - } - if (!empty($options['tab_width'])) { $this->tab_width = ' --tab-width='.$options['tab_width']; } @@ -125,20 +117,15 @@ class PhpCodeSniffer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $this->encoding = ' --encoding=' . $options['encoding']; } - if (isset($options['path'])) { - $this->path = $options['path']; - } + $this->setOptions($options); + } - if (isset($options['ignore'])) { - $this->ignore = $options['ignore']; - } - - if (isset($options['allowed_warnings'])) { - $this->allowed_warnings = (int)$options['allowed_warnings']; - } - - if (isset($options['allowed_errors'])) { - $this->allowed_errors = (int)$options['allowed_errors']; + protected function setOptions($options) + { + foreach (array('directory', 'standard', 'path', 'ignore', 'allowed_warnings', 'allowed_errors') as $key) { + if (array_key_exists($key, $options)) { + $this->{$key} = $options[$key]; + } } } diff --git a/PHPCI/Plugin/PhpCpd.php b/PHPCI/Plugin/PhpCpd.php index 2e0b908e..9325efa6 100644 --- a/PHPCI/Plugin/PhpCpd.php +++ b/PHPCI/Plugin/PhpCpd.php @@ -1,11 +1,11 @@ +* @package PHPCI +* @subpackage Plugins +*/ +class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin +{ + /** + * @var \PHPCI\Builder + */ + protected $phpci; + + /** + * @var \PHPCI\Model\Build + */ + protected $build; + + /** + * @var string Based on the assumption the root may not hold the code to be + * tested, extends the build path. + */ + protected $path; + + /** + * @var array - paths to ignore + */ + protected $ignore; + + protected $skipClasses = false; + protected $skipMethods = false; + + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test') { + return true; + } + + return false; + } + + + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $this->phpci = $phpci; + $this->build = $build; + $this->ignore = $phpci->ignore; + $this->path = ''; + $this->allowed_warnings = 0; + + if (isset($options['zero_config']) && $options['zero_config']) { + $this->allowed_warnings = -1; + } + + if (array_key_exists('skip_classes', $options)) { + $this->skipClasses = true; + } + + if (array_key_exists('skip_methods', $options)) { + $this->skipMethods = true; + } + + if (!empty($options['path'])) { + $this->path = $options['path']; + } + + if (array_key_exists('allowed_warnings', $options)) { + $this->allowed_warnings = (int)$options['allowed_warnings']; + } + } + + /** + * Runs PHP Mess Detector in a specified directory. + */ + public function execute() + { + // Check that the binary exists: + $checker = $this->phpci->findBinary('phpdoccheck'); + + if (!$checker) { + $this->phpci->logFailure('Could not find phpdoccheck.'); + return false; + } + + // Build ignore string: + $ignore = ''; + if (count($this->ignore)) { + $ignore = ' --exclude="' . implode(',', $this->ignore) . '"'; + } + + // Are we skipping any checks? + $add = ''; + if ($this->skipClasses) { + $add .= ' --skip-classes'; + } + + if ($this->skipMethods) { + $add .= ' --skip-methods'; + } + + // Build command string: + $path = $this->phpci->buildPath . $this->path; + $cmd = $checker . ' --json --directory="%s"%s%s'; + + // Disable exec output logging, as we don't want the XML report in the log: + $this->phpci->logExecOutput(false); + + // Run checker: + $this->phpci->executeCommand( + $cmd, + $path, + $ignore, + $add + ); + + // Re-enable exec output logging: + $this->phpci->logExecOutput(true); + + $output = json_decode($this->phpci->getLastOutput()); + $errors = count($output); + $success = true; + + $this->build->storeMeta('phpdoccheck-warnings', $errors); + $this->build->storeMeta('phpdoccheck-data', $output); + + if ($this->allowed_warnings != -1 && $errors > $this->allowed_warnings) { + $success = false; + } + + return $success; + } +} diff --git a/PHPCI/Plugin/PhpLoc.php b/PHPCI/Plugin/PhpLoc.php index 000b82cc..26ec8204 100644 --- a/PHPCI/Plugin/PhpLoc.php +++ b/PHPCI/Plugin/PhpLoc.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Plugin; @@ -68,7 +68,7 @@ class PhpLoc implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin return false; } - $success = $this->phpci->executeCommand($phploc . ' %s "%s"', $ignore, $this->phpci->buildPath); + $success = $this->phpci->executeCommand($phploc . ' %s "%s"', $ignore, $this->directory); $output = $this->phpci->getLastOutput(); if (preg_match_all('/\((LOC|CLOC|NCLOC|LLOC)\)\s+([0-9]+)/', $output, $matches)) { diff --git a/PHPCI/Plugin/PhpMessDetector.php b/PHPCI/Plugin/PhpMessDetector.php index 2be7fed4..deb6eafd 100755 --- a/PHPCI/Plugin/PhpMessDetector.php +++ b/PHPCI/Plugin/PhpMessDetector.php @@ -1,11 +1,11 @@ ignore)) { - $ignore = ' --exclude ' . implode(',', $this->ignore); - } - - $suffixes = ''; - if (count($this->suffixes)) { - $suffixes = ' --suffixes ' . implode(',', $this->suffixes); - } - - if (!empty($this->rules) && !is_array($this->rules)) { - $this->phpci->logFailure('The "rules" option must be an array.'); + if (!$this->tryAndProcessRules()) { return false; } - foreach ($this->rules as &$rule) { - if (strpos($rule, '/') !== false) { - $rule = $this->phpci->buildPath . $rule; - } - } + $phpmdBinaryPath = $this->phpci->findBinary('phpmd'); - $phpmd = $this->phpci->findBinary('phpmd'); - - if (!$phpmd) { + if (!$phpmdBinaryPath) { $this->phpci->logFailure('Could not find phpmd.'); return false; } - - $path = $this->phpci->buildPath . $this->path; - if (!empty($this->path) && $this->path{0} == '/') { - $path = $this->path; - } - $cmd = $phpmd . ' "%s" xml %s %s %s'; + $this->executePhpMd($phpmdBinaryPath); - // Disable exec output logging, as we don't want the XML report in the log: - $this->phpci->logExecOutput(false); - - // Run PHPMD: - $this->phpci->executeCommand( - $cmd, - $path, - implode(',', $this->rules), - $ignore, - $suffixes - ); - - // Re-enable exec output logging: - $this->phpci->logExecOutput(true); - - $success = true; - - list($errors, $data) = $this->processReport(trim($this->phpci->getLastOutput())); - $this->build->storeMeta('phpmd-warnings', $errors); + list($errorCount, $data) = $this->processReport(trim($this->phpci->getLastOutput())); + $this->build->storeMeta('phpmd-warnings', $errorCount); $this->build->storeMeta('phpmd-data', $data); - if ($this->allowed_warnings != -1 && $errors > $this->allowed_warnings) { - $success = false; - } - - return $success; + return $this->wasLastExecSuccessful($errorCount); } protected function overrideSetting($options, $key) @@ -200,4 +157,79 @@ class PhpMessDetector implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin return array($warnings, $data); } + + protected function tryAndProcessRules() + { + if (!empty($this->rules) && !is_array($this->rules)) { + $this->phpci->logFailure('The "rules" option must be an array.'); + return false; + } + + foreach ($this->rules as &$rule) { + if (strpos($rule, '/') !== false) { + $rule = $this->phpci->buildPath . $rule; + } + } + + return true; + } + + protected function executePhpMd($binaryPath) + { + $cmd = $binaryPath . ' "%s" xml %s %s %s'; + + $path = $this->getTargetPath(); + + $ignore = ''; + if (count($this->ignore)) { + $ignore = ' --exclude ' . implode(',', $this->ignore); + } + + $suffixes = ''; + if (count($this->suffixes)) { + $suffixes = ' --suffixes ' . implode(',', $this->suffixes); + } + + // Disable exec output logging, as we don't want the XML report in the log: + $this->phpci->logExecOutput(false); + + // Run PHPMD: + $this->phpci->executeCommand( + $cmd, + $path, + implode(',', $this->rules), + $ignore, + $suffixes + ); + + // Re-enable exec output logging: + $this->phpci->logExecOutput(true); + } + + protected function getTargetPath() + { + $path = $this->phpci->buildPath . $this->path; + if (!empty($this->path) && $this->path{0} == '/') { + $path = $this->path; + return $path; + } + return $path; + } + + /** + * Returns a boolean indicating if the error count can be considered a success. + * + * @param int $errorCount + * @return bool + */ + protected function wasLastExecSuccessful($errorCount) + { + $success = true; + + if ($this->allowed_warnings != -1 && $errorCount > $this->allowed_warnings) { + $success = false; + return $success; + } + return $success; + } } diff --git a/PHPCI/Plugin/PhpParallelLint.php b/PHPCI/Plugin/PhpParallelLint.php index 282098cc..30398995 100644 --- a/PHPCI/Plugin/PhpParallelLint.php +++ b/PHPCI/Plugin/PhpParallelLint.php @@ -1,11 +1,11 @@ phpci->executeCommand($phpspec . ' --format=pretty --no-code-generation'); + $success = $this->phpci->executeCommand($phpspec . ' --format=pretty --no-code-generation run'); chdir($curdir); diff --git a/PHPCI/Plugin/PhpUnit.php b/PHPCI/Plugin/PhpUnit.php index 16de7aa0..acefbcc0 100755 --- a/PHPCI/Plugin/PhpUnit.php +++ b/PHPCI/Plugin/PhpUnit.php @@ -1,11 +1,11 @@ args = $options['args']; + $this->args = $this->phpci->interpolate($options['args']); } if (isset($options['path'])) { @@ -132,9 +132,16 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $success &= $this->runDir($this->directory); } - $output = $this->phpci->getLastOutput(); - $tapParser = new TapParser($output); - $output = $tapParser->parse(); + $tapString = $this->phpci->getLastOutput(); + + try { + $tapParser = new TapParser($tapString); + $output = $tapParser->parse(); + } catch (\Exception $ex) { + $this->phpci->logFailure($tapString); + throw $ex; + } + $failures = $tapParser->getTotalFailures(); $this->build->storeMeta('phpunit-errors', $failures); diff --git a/PHPCI/Plugin/Shell.php b/PHPCI/Plugin/Shell.php index 73439cad..fceea3e2 100644 --- a/PHPCI/Plugin/Shell.php +++ b/PHPCI/Plugin/Shell.php @@ -2,9 +2,9 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2013, Block 8 Limited. + * @copyright Copyright 2014, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md - * @link http://www.phptesting.org/ + * @link https://www.phptesting.org/ */ namespace PHPCI\Plugin; diff --git a/PHPCI/Plugin/Sqlite.php b/PHPCI/Plugin/Sqlite.php new file mode 100644 index 00000000..1ffdcc88 --- /dev/null +++ b/PHPCI/Plugin/Sqlite.php @@ -0,0 +1,81 @@ + +* @package PHPCI +* @subpackage Plugins +*/ +class Sqlite implements \PHPCI\Plugin +{ + /** + * @var \PHPCI\Builder + */ + protected $phpci; + + /** + * @var \PHPCI\Model\Build + */ + protected $build; + + /** + * @var array + */ + protected $queries = array(); + + /** + * @var string + */ + protected $path; + + /** + * @param Builder $phpci + * @param Build $build + * @param array $options + */ + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $this->phpci = $phpci; + $this->build = $build; + $this->queries = $options; + $buildSettings = $phpci->getConfig('build_settings'); + + if (isset($buildSettings['sqlite'])) { + $sql = $buildSettings['sqlite']; + $this->path = $sql['path']; + } + } + + /** + * Connects to SQLite and runs a specified set of queries. + * @return boolean + */ + public function execute() + { + try { + $opts = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $pdo = new PDO('sqlite:' . $this->path, $opts); + + foreach ($this->queries as $query) { + $pdo->query($query); + } + } catch (\Exception $ex) { + $this->phpci->logFailure($ex->getMessage()); + return false; + } + return true; + } +} diff --git a/PHPCI/Plugin/Util/TapParser.php b/PHPCI/Plugin/Util/TapParser.php index 595e83ef..5b099b03 100644 --- a/PHPCI/Plugin/Util/TapParser.php +++ b/PHPCI/Plugin/Util/TapParser.php @@ -7,12 +7,12 @@ class TapParser const TEST_COUNTS_PATTERN = '/([0-9]+)\.\.([0-9]+)/'; const TEST_LINE_PATTERN = '/(ok|not ok)\s+[0-9]+\s+\-\s+([^\n]+)::([^\n]+)/'; const TEST_MESSAGE_PATTERN = '/message\:\s+\'([^\']+)\'/'; + const TEST_COVERAGE_PATTERN = '/Generating code coverage report/'; /** * @var string */ protected $tapString; - protected $failures = 0; /** @@ -43,16 +43,23 @@ class TapParser throw new \Exception('TapParser only supports TAP version 13'); } + if (preg_match(self::TEST_COVERAGE_PATTERN, $lines[count($lines) - 1])) { + array_pop($lines); + if ($lines[count($lines) - 1] == "") { + array_pop($lines); + } + } + $matches = array(); $totalTests = 0; if (preg_match(self::TEST_COUNTS_PATTERN, $lines[0], $matches)) { array_shift($lines); - $totalTests = (int)$matches[2]; + $totalTests = (int) $matches[2]; } if (preg_match(self::TEST_COUNTS_PATTERN, $lines[count($lines) - 1], $matches)) { array_pop($lines); - $totalTests = (int)$matches[2]; + $totalTests = (int) $matches[2]; } $rtn = $this->processTestLines($lines); diff --git a/PHPCI/Plugin/Wipe.php b/PHPCI/Plugin/Wipe.php index 7a0c2107..e87800b4 100644 --- a/PHPCI/Plugin/Wipe.php +++ b/PHPCI/Plugin/Wipe.php @@ -1,11 +1,11 @@ directory)) { - $cmd = 'rm -rf %s*'; + $cmd = 'rm -Rf "%s"'; + if (IS_WIN) { + $cmd = 'rmdir /S /Q "%s"'; + } $success = $this->phpci->executeCommand($cmd, $this->directory); } return $success; diff --git a/PHPCI/Store.php b/PHPCI/Store.php index 6806a097..bb8e4a21 100644 --- a/PHPCI/Store.php +++ b/PHPCI/Store.php @@ -1,4 +1,12 @@ prepare($query); + $stmt->bindValue(':project_id', $value); + + if ($stmt->execute()) { + $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + $map = function ($item) { + return new BuildMeta($item); + }; + $rtn = array_map($map, $res); + + return array('items' => $rtn, 'count' => $count); + } else { + return array('items' => array(), 'count' => 0); + } + } + public function getByBuildId($value, $limit = null, $useConnection = 'read') { if (is_null($value)) { diff --git a/PHPCI/Store/BuildMetaStore.php b/PHPCI/Store/BuildMetaStore.php index ad17ef93..9800dcad 100644 --- a/PHPCI/Store/BuildMetaStore.php +++ b/PHPCI/Store/BuildMetaStore.php @@ -1,7 +1,10 @@ prepare($query); + $stmt = Database::getConnection('read')->prepare($query); $stmt->bindValue(':pid', $projectId); if ($stmt->execute()) { @@ -39,6 +41,28 @@ class BuildStore extends BuildStoreBase } } + public function getByProjectAndCommit($projectId, $commitId) + { + $query = 'SELECT * FROM `build` WHERE `project_id` = :project_id AND `commit_id` = :commit_id'; + $stmt = Database::getConnection('read')->prepare($query); + $stmt->bindValue(':project_id', $projectId); + $stmt->bindValue(':commit_id', $commitId); + + if ($stmt->execute()) { + $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + $map = function ($item) { + return new Build($item); + }; + + $rtn = array_map($map, $res); + + return array('items' => $rtn, 'count' => count($rtn)); + } else { + return array('items' => array(), 'count' => 0); + } + } + public function getMeta($key, $projectId, $buildId = null, $numResults = 1) { $select = '`build_id`, `meta_key`, `meta_value`'; @@ -46,7 +70,7 @@ class BuildStore extends BuildStoreBase $where = '`meta_key` = :key AND `project_id` = :projectId ' . $and; $query = 'SELECT '.$select.' FROM `build_meta` WHERE '.$where.' ORDER BY id DESC LIMIT :numResults'; - $stmt = \b8\Database::getConnection('read')->prepare($query); + $stmt = Database::getConnection('read')->prepare($query); $stmt->bindValue(':key', $key, \PDO::PARAM_STR); $stmt->bindValue(':projectId', (int)$projectId, \PDO::PARAM_INT); $stmt->bindValue(':buildId', (int)$buildId, \PDO::PARAM_INT); @@ -77,7 +101,7 @@ class BuildStore extends BuildStoreBase $cols = '`project_id`, `build_id`, `meta_key`, `meta_value`'; $query = 'REPLACE INTO build_meta ('.$cols.') VALUES (:projectId, :buildId, :key, :value)'; - $stmt = \b8\Database::getConnection('read')->prepare($query); + $stmt = Database::getConnection('read')->prepare($query); $stmt->bindValue(':key', $key, \PDO::PARAM_STR); $stmt->bindValue(':projectId', (int)$projectId, \PDO::PARAM_INT); $stmt->bindValue(':buildId', (int)$buildId, \PDO::PARAM_INT); diff --git a/PHPCI/Store/ProjectStore.php b/PHPCI/Store/ProjectStore.php index c844d756..0596d0c9 100644 --- a/PHPCI/Store/ProjectStore.php +++ b/PHPCI/Store/ProjectStore.php @@ -1,11 +1,11 @@ getStatus()) getBranch(); ?> - getPlugins(), true); - - if ( !is_array($plugins) ) { - $plugins = array(); - } - if ( 0 === count($plugins) ) { - ?> - $pluginstatus): - $subcls = $pluginstatus?'label label-success':'label label-danger'; - ?> Build()->formatPluginName($plugin); ?> -
+
diff --git a/PHPCI/View/Email/failed.phtml b/PHPCI/View/Email/failed.phtml index fc199540..4391748f 100644 --- a/PHPCI/View/Email/failed.phtml +++ b/PHPCI/View/Email/failed.phtml @@ -1,15 +1,15 @@
- getTitle(); ?> - Build #getId(); ?> + getTitle(); ?> - Build #getId(); ?>
-

Your commit getCommitId(); ?> caused a failed build in project getTitle(); ?>.

+

Your commit getCommitId(); ?> caused a failed build in project getTitle(); ?>.

-

getCommitMessage(); ?>

+

getCommitMessage(); ?>

-

Please review your commit and the build log.

+

Please review your commit and the build log.

\ No newline at end of file diff --git a/PHPCI/View/Plugin/index.phtml b/PHPCI/View/Plugin/index.phtml index 3cfac153..a224c752 100644 --- a/PHPCI/View/Plugin/index.phtml +++ b/PHPCI/View/Plugin/index.phtml @@ -1,8 +1,4 @@

Packages and Provided Plugins

- -

PHPCI cannot automatically install/remove plugins for you, as either the shell_exec() - function is disabled or PHPCI could not find Composer. PHPCI will update composer.json for you, but you will need to run Composer manually to make the changes.

-

PHPCI cannot update composer.json for you as it is not writable.

@@ -12,12 +8,8 @@

has been removed.

- -

has been installed.

- - -

has been added to composer.json and will be installed next time you run composer update.

+

has been added to composer.json for you and will be installed next time you run composer update.

@@ -34,9 +26,9 @@ - name; ?> - class; ?> - source; ?> + name; ?> + class; ?> + source; ?> diff --git a/PHPCI/View/Session.phtml b/PHPCI/View/Session.phtml new file mode 100644 index 00000000..ed75d2b4 --- /dev/null +++ b/PHPCI/View/Session.phtml @@ -0,0 +1,88 @@ + + + + Log in to PHPCI + + + + + + + + + + +
+
+ +
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/PHPCI/View/Session/forgotPassword.phtml b/PHPCI/View/Session/forgotPassword.phtml new file mode 100644 index 00000000..a9897bbd --- /dev/null +++ b/PHPCI/View/Session/forgotPassword.phtml @@ -0,0 +1,33 @@ + +

+ We've emailed you a link to reset your password. +

+ + +
+
+ Don't worry!
Just enter your email address below and we'll email you a link to reset your password. +
+ +
+
+ +
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/PHPCI/View/Session/login.phtml b/PHPCI/View/Session/login.phtml index 2314a639..bc2f16e1 100644 --- a/PHPCI/View/Session/login.phtml +++ b/PHPCI/View/Session/login.phtml @@ -1,91 +1,6 @@ - - - - Log in to PHPCI + +

Incorrect email address or password

+ + - - - - - - - - - -
-
- -
- -

Incorrect email address or password

- - -
- - -
-
- - +Forgotten your password? \ No newline at end of file diff --git a/PHPCI/View/Session/resetPassword.phtml b/PHPCI/View/Session/resetPassword.phtml new file mode 100644 index 00000000..9544879e --- /dev/null +++ b/PHPCI/View/Session/resetPassword.phtml @@ -0,0 +1,27 @@ + + +
+
+ Please enter a new password +
+ +
+
+
+ + +
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/PHPCI/View/Settings/index.phtml b/PHPCI/View/Settings/index.phtml index 3f0eb5ec..322206c4 100644 --- a/PHPCI/View/Settings/index.phtml +++ b/PHPCI/View/Settings/index.phtml @@ -10,6 +10,12 @@

+ +

+ PHPCI cannot write to your config.yml file, settings may not be saved properly until this is rectified. +

+ +

Your Github account has been linked. diff --git a/PHPCI/View/User/profile.phtml b/PHPCI/View/User/profile.phtml new file mode 100644 index 00000000..5e006738 --- /dev/null +++ b/PHPCI/View/User/profile.phtml @@ -0,0 +1,15 @@ +

User()->getName(); ?>

+ + +

Your details have been updated.

+ + +
+
+

Update your details

+
+
+ +
+
+ diff --git a/PHPCI/View/exception.phtml b/PHPCI/View/exception.phtml new file mode 100644 index 00000000..1ecbe6a4 --- /dev/null +++ b/PHPCI/View/exception.phtml @@ -0,0 +1,9 @@ +
+
+

Sorry, there was a problem

+
+ +
+ getMessage(); ?> +
+
\ No newline at end of file diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 58b78cc2..ed3e539e 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -37,13 +37,27 @@